Authentication
The Filter King API uses OAuth2 to authenticate requests. You can view and manage your API credentials in your dashboard -> API Management.
Your API keys carry many privileges, so be sure to keep them secure! Do not share your secret API keys in publicly accessible areas such as GitHub, client-side code, and so forth.
All API requests must be made over HTTPS. Calls made over plain HTTP will fail. API requests without authentication will also fail.
Authentication Flow
To authenticate, you'll need to obtain an access token using your client credentials. This token
should be included in the Authorization
header of your requests.
# Exchange client credentials for an access token
curl 'https://filterking.com/oauth/token' \
-H 'Content-Type: application/json' \
-d '{
"grant_type": "client_credentials",
"client_id": "your_client_id",
"client_secret": "your_client_secret"
}'
<?php
$client = new GuzzleHttp\Client();
$response = $client->post('https://filterking.com/oauth/token', [
'json' => [
'grant_type' => 'client_credentials',
'client_id' => 'your_client_id',
'client_secret' => 'your_client_secret'
]
]);
$token = json_decode($response->getBody(), true);
$accessToken = $token['access_token'];
const response = await fetch('https://filterking.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: 'your_client_id',
client_secret: 'your_client_secret'
})
});
const data = await response.json();
const accessToken = data.access_token;
import requests
response = requests.post('https://filterking.com/oauth/token', json={
'grant_type': 'client_credentials',
'client_id': 'your_client_id',
'client_secret': 'your_client_secret'
})
token = response.json()
access_token = token['access_token']
Using Your Access Token
Include the bearer token in the Authorization
header when making API requests:
curl https://filterking.com/api/v1/info \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
<?php
$response = $client->get('https://filterking.com/api/v1/info', [
'headers' => [
'Authorization' => 'Bearer ' . $accessToken,
]
]);
$partner = json_decode($response->getBody(), true);
const response = await fetch('https://filterking.com/api/v1/info', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
const partner = await response.json();
headers = {
'Authorization': f'Bearer {access_token}'
}
response = requests.get(
'https://filterking.com/api/v1/info',
headers=headers
)
partner = response.json()
Idempotency
To prevent duplicate orders caused by network retries or accidental double-submissions, the API supports
idempotency for POST requests. Specify a unique Idempotency-Key
header to ensure that a request is only executed once.
Subsequent requests with the same key will return the cached response from the original request, preventing duplicate order creation. Keys are stored for 48 hours.
Generating an Idempotency Key
We recommend using a UUID v4 for your idempotency keys. UUIDs provide uniqueness and are widely supported across programming languages.
# Generate UUID v4 in Linux/macOS
UUID=$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen)
echo $UUID
# Or using OpenSSL
openssl rand -hex 16 | sed 's/\(.\{8\}\)\(.\{4\}\)\(.\{4\}\)\(.\{4\}\)\(.\{12\}\)/\1-\2-\3-\4-\5/'
<?php
// Generate UUID v4 in PHP
$idempotencyKey = sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
// Or use Ramsey UUID (recommended)
// composer require ramsey/uuid
use Ramsey\Uuid\Uuid;
$idempotencyKey = Uuid::uuid4()->toString();
// Generate UUID v4 in JavaScript (Node.js / Browser)
const idempotencyKey = crypto.randomUUID();
// Or use a simple fallback for older browsers
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
const idempotencyKey = generateUUID();
import uuid
# Generate UUID v4 in Python
idempotency_key = str(uuid.uuid4())
Usage Example
Include the generated key in your request headers:
Authorization: Bearer YOUR_ACCESS_TOKEN
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
Errors
Filter King uses conventional HTTP response codes to indicate the success or failure of an API request. In general:
- Codes in the 2xx range indicate success.
- Codes in the 4xx range indicate an error that failed given the information provided (e.g., a required parameter was omitted, a resource was not found, etc.).
- Codes in the 5xx range indicate an error with Filter King's servers.
HTTP Status Code Summary
| Status Code | Description |
|---|---|
| 200 OK | Everything worked as expected. |
| 201 Created | The resource was successfully created. |
| 400 Bad Request | The request was unacceptable, often due to missing a required parameter. |
| 401 Unauthorized | No valid API key or access token provided. |
| 403 Forbidden | The API key doesn't have permissions to perform the request. |
| 404 Not Found | The requested resource doesn't exist. |
| 422 Unprocessable | The request was well-formed but contains semantic errors. |
| 429 Too Many Requests | Too many requests hit the API too quickly. |
| 500 Server Error | Something went wrong on Filter King's end. |
Error Response Format
Errors are returned in JSON format with the following structure:
{
"success": false,
"message": "The requested resource was not found.",
"errors": [
{
"field": "order_id",
"message": "The selected order id is invalid."
}
]
}
Rate Limiting
To ensure fair usage and system stability, the API implements rate limiting on requests.
| Attribute | Limit |
|---|---|
| Requests per minute | 60 requests |
When you exceed the rate limit, the API returns a 429 Too Many Requests status code.
Recommendation: Implement exponential backoff (retry with increasing delays) when encountering rate limit errors to handle temporary limits gracefully. If you need to extend rate limit please contact our support.
General Notes
Important conventions and standards used throughout the API:
Date & Time Format
All date and time fields are returned in UTC using the ISO 8601 format
(YYYY-MM-DDTHH:MM:SS.ffffffZ).
Example: 2026-02-21T19:58:46.000000Z
Note: Always parse dates as UTC. The Z suffix indicates UTC timezone.
Data Sync Strategy
To ensure efficient data synchronization and optimal API performance, we recommend following the below guidelines when integrating with our API.
Recommended Synchronization Approach
Use the appropriate endpoint based on your specific use case to minimize API calls and ensure data freshness.
Individual Order Endpoints
Use individual order endpoints when you need to retrieve or display real-time details for a specific order. These endpoints are designed for on-demand requests, such as:
- Loading order details on an order details page
- Displaying the latest tracking information for a single order
- Responding to user-initiated actions
GET /api/v1/orders/{id}
GET /api/v1/orders/{id}/tracking
Bulk Data Synchronization
For maintaining up-to-date order data across your system, use the list endpoint with the updated_after parameter. This approach is recommended for:
- Periodic background synchronization
- Batch updates of multiple orders
- Tracking status updates across your order database
GET /api/v1/orders?updated_after=2026-02-21T19:58:46.000000Z
Implementation Guidelines
Recommended Polling Interval
We recommend calling the bulk synchronization endpoint every 15 minutes. This interval balances data freshness with API resource efficiency.
Tracking Last Sync Timestamp
Maintain the timestamp of your last successful sync and use it as the updated_after parameter value in subsequent requests. This ensures you only receive orders that have been updated since your last sync.
Error Handling
Implement exponential backoff when encountering rate limits or temporary errors. If the sync fails, retry with increasing delays (e.g., 1 minute, 5 minutes, 15 minutes) before reverting to the normal 15-minute interval.
Benefit: Following this strategy reduces unnecessary API calls, ensures you receive only relevant updated data, and helps maintain optimal system performance for both your application and our API.
Example Implementation Flow
# Store last sync timestamp (example)
LAST_SYNC="2026-02-21T19:58:46.000000Z"
# Poll every 15 minutes for updated orders
curl "https://filterking.com/api/v1/orders?updated_after=${LAST_SYNC}&per_page=100" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
<?php
// Store and retrieve last sync timestamp from your database/config
$lastSyncTimestamp = getLastSyncTimestamp(); // e.g., "2026-02-21T19:58:46.000000Z"
// Poll for updated orders (run every 15 minutes via cron/job scheduler)
$response = $client->get('https://filterking.com/api/v1/orders', [
'headers' => [
'Authorization' => 'Bearer ' . $accessToken,
],
'query' => [
'updated_after' => $lastSyncTimestamp,
'per_page' => 100
]
]);
$orders = json_decode($response->getBody(), true);
// Process orders and update last sync timestamp
if ($orders['success']) {
foreach ($orders['data']['orders'] as $order) {
// Sync order data to your database
syncOrderToDatabase($order);
}
// Update last sync timestamp to current time
updateLastSyncTimestamp(date('c'));
}
// Store last sync timestamp
let lastSyncTimestamp = '2026-02-21T19:58:46.000000Z';
// Function to sync updated orders (run every 15 minutes)
async function syncOrders() {
const response = await fetch(
`https://filterking.com/api/v1/orders?updated_after=${lastSyncTimestamp}&per_page=100`,
{
headers: {
'Authorization': `Bearer ${accessToken}`
}
}
);
const data = await response.json();
if (data.success) {
// Process each updated order
for (const order of data.data.orders) {
await syncOrderToDatabase(order);
}
// Update last sync timestamp to current time
lastSyncTimestamp = new Date().toISOString();
}
}
// Run every 15 minutes
setInterval(syncOrders, 15 * 60 * 1000);
import requests
from datetime import datetime
import time
# Store last sync timestamp
last_sync_timestamp = '2026-02-21T19:58:46.000000Z'
def sync_orders():
global last_sync_timestamp
headers = {
'Authorization': f'Bearer {access_token}'
}
params = {
'updated_after': last_sync_timestamp,
'per_page': 100
}
response = requests.get(
'https://filterking.com/api/v1/orders',
headers=headers,
params=params
)
data = response.json()
if data['success']:
for order in data['data']['orders']:
# Sync order to your database
sync_order_to_database(order)
# Update last sync timestamp
last_sync_timestamp = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ')
# Run every 15 minutes
while True:
sync_orders()
time.sleep(15 * 60) # 15 minutes
Order Statuses
The following order statuses are used throughout the API to indicate the current state of an order.
Status List
| Status | Description |
|---|---|
paid |
Order has been paid and is ready for processing |
unfulfilled |
Order has started processing |
partially shipped |
Some items in the order have been shipped, but not all |
fulfilled |
All order items have been shipped and processing is complete |
ready for pick up |
Order is ready for customer pick up (for customer_pick_up shipping method) |
refunded |
Order has been fully or partially refunded |
cancelled |
Order has been cancelled |
cancelled and refunded |
Order was cancelled and a refund was issued |
returned and refunded |
Order items were returned and a refund was issued |
error |
Order processing encountered an error |
POST Issue an access token
Exchange client credentials for an OAuth2 access token.
POST /oauth/token
Parameters
| Parameter | Type | Description |
|---|---|---|
grant_type |
string |
Must be "client_credentials" |
client_id |
string |
Your OAuth client ID |
client_secret |
string |
Your OAuth client secret |
Response
{
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
}
Token Validity: The access token is valid for 1 hour (3600 seconds). After expiration, you need to request a new token using the same endpoint.
GET Get partner information
Retrieve information about the authenticated partner account.
GET /api/v1/info
curl https://filterking.com/api/v1/info \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
<?php
$response = $client->get('https://filterking.com/api/v1/info', [
'headers' => [
'Authorization' => 'Bearer ' . $accessToken,
]
]);
$partner = json_decode($response->getBody(), true);
const response = await fetch('https://filterking.com/api/v1/info', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
const partner = await response.json();
headers = {
'Authorization': f'Bearer {access_token}'
}
response = requests.get(
'https://filterking.com/api/v1/info',
headers=headers
)
partner = response.json()
Response
{
"success": true,
"data": {
"app_name": "FK API",
"environment": "production",
"client_id": "your_client_id",
"partner_name": "Filter King Partner",
"partner_email": "[email protected]",
"partner_phone": "123-456-7890",
"api_status": "active"
}
}
GET Get All Parent Models
Retrieve full list of available stock inventory SKUs
GET /api/v1/get-all-parent-models
curl https://filterking.com/api/v1/get-all-parent-models \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
<?php
$response = $client->get('https://filterking.com/api/v1/get-all-parent-models', [
'headers' => [
'Authorization' => 'Bearer ' . $accessToken,
]
]);
$partner = json_decode($response->getBody(), true);
const response = await fetch('https://filterking.com/api/v1/get-all-parent-models', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
const partner = await response.json();
headers = {
'Authorization': f'Bearer {access_token}'
}
response = requests.get(
'https://filterking.com/api/v1/get-all-parent-models',
headers=headers
)
partner = response.json()
Response
{
"success": true,
"data": {
"sku_items": [
{
"parent_model": "AF10x10x1-M13",
"size": "10x10x1",
"actual_size": "9.5 x 9.5 x .75",
"merv": "MERV 13",
"thickness": "1-Inch Filter",
"unit_price": 7.3
},
{
"parent_model": "AF10x10x1-M11",
"size": "10x10x1",
"actual_size": "9.5 x 9.5 x .75",
"merv": "MERV 11",
"thickness": "1-Inch Filter",
"unit_price": 7.12
},
.
.
.
{
"parent_model": "AF20X20X5-HW-CO",
"size": "20x20x5",
"actual_size": "19.68 x 19.93 x 4.37",
"merv": "CARBON",
"thickness": "5-Inch Filter",
"unit_price": 20.37
}
],
"total": 315
}
}
POST Get order quotes
Get order quotes including estimated shipping costs and Sales Tax.
Note: Actual shipping costs may vary at the time of shipment. A ship_to address is required to calculate shipping cost and sales tax. For customer_pick_up, pass your company address as the ship_to parameter. If your company has an approved tax-exempt document on file, no sales tax will be calculated.
POST /api/v1/order/quotes
curl https://filterking.com/api/v1/order/quotes \
-X POST \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: your-unique-key-here" \
-d '{
"shipping_method": "fedex",
"ship_to": {
"address_line_1": "123 Main St",
"address_line_2": "Apt 123",
"city": "Anytown",
"state": "NY",
"zip": "12345",
"country": "US"
},
"items": [
{
"parent_model": "AF12x12x1-M8",
"quantity": 4
},
{
"parent_model": "AF10x10x1A-M8",
"quantity": 10
}
]
}'
<?php
$client = new GuzzleHttp\Client();
$response = $client->post('https://filterking.com/api/v1/order/quotes', [
'headers' => [
'Authorization' => 'Bearer ' . $accessToken,
'Idempotency-Key' => 'your-unique-key-here',
],
'json' => [
'shipping_method': 'fedex',
'ship_to' => [
"address_line_1": "123 Main St",
"address_line_2": "Apt 123",
"city": "Anytown",
"state": "NY",
"zip": "12345",
"country": "US"
],
'items': [
{
'parent_model': 'AF12x12x1-M8',
'quantity': 4
},
{
'parent_model': 'AF10x10x1A-M8',
'quantity': 10
}
]
]
]);
$quotes = json_decode($response->getBody(), true);
const response = await fetch('https://filterking.com/api/v1/order/quotes', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'your-unique-key-here',
},
body: JSON.stringify({
'shipping_method': 'fedex',
'ship_to': {
"address_line_1": "123 Main St",
"address_line_2": "Apt 123",
"city": "Anytown",
"state": "NY",
"zip": "12345",
"country": "US"
},
'items': [
{
'parent_model': 'AF12x12x1-M8',
'quantity': 4
},
{
'parent_model': 'AF10x10x1A-M8',
'quantity': 10
}
]
})
});
const quotes = await response.json();
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json',
'Idempotency-Key': 'your-unique-key-here'
}
data = {
'shipping_method': 'fedex',
'ship_to': {
"address_line_1": "123 Main St",
"address_line_2": "Apt 123",
"city": "Anytown",
"state": "NY",
"zip": "12345",
"country": "US"
},
'items': [
{
'parent_model': 'AF12x12x1-M8',
'quantity': 4
},
{
'parent_model': 'AF10x10x1A-M8',
'quantity': 10
}
]
}
response = requests.post(
'https://filterking.com/api/v1/order/quotes',
headers=headers,
json=data
)
quotes = response.json()
Parameters
| Parameter | Type | Description | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
shipping_method |
string |
Shipping carrier method: fedex or customer_pick_up. If customer_pick_up is passed, estimated_shipping_cost will be 0. |
||||||||||||||||||||||||
ship_to |
object |
Shipping destination address (required)
Note: Please pass your company address if you choose shipping_method as
customer_pick_up
|
||||||||||||||||||||||||
items |
array |
Array of filter items
|
||||||||||||||||||||||||
Accepted MERV ratings: M8 (MERV 8), M11 (MERV 11), M13 (MERV 13), CO (Carbon)
Thickness: Only 0.5, 1, 2, or 4 inches are accepted.
Width and height measurements must use standard fractional increments. Accepted decimal values: .125 (1/8"), .25 (1/4"), .375 (3/8"), .5 (1/2"), .625 (5/8"), .75 (3/4"), or .875 (7/8").
Example: AF12.125x12.75x1-M8
For actual size filters, append A after thickness (e.g., AF12.125x12.75x1A-M8).
Filter Size Quick Reference
Common filter sizes and their corresponding parent model format:
| Filter Size | Parent Model (MERV 8) | Parent Model (MERV 11) |
|---|---|---|
20x20x1 |
AF20x20x1-M8 |
AF20x20x1-M11 |
16x25x1 |
AF16x25x1-M8 |
AF16x25x1-M11 |
14x25x2 |
AF14x25x2-M8 |
AF14x25x2-M11 |
12x12x1 |
AF12x12x1-M8 |
AF12x12x1-M11 |
18.25x23.5x1 |
AF18.25x23.5x1-M8 |
AF18.25x23.5x1-M11 |
15.375x24.875x1 |
AF15.375x24.875x1-M8 |
AF15.375x24.875x1-M11 |
14.25x20x1 |
AF14.25x20x1-M8 |
AF14.25x20x1-M11 |
12.125x12.75x1a (actual size) |
AF12.125x12.75x1A-M8 |
AF12.125x12.75x1A-M11 |
11.625x14.5x2a (actual size) |
AF11.625x14.5x2A-M8 |
AF11.625x14.5x2A-M11 |
10x10x4 |
AF10x10x4-M8 |
AF10x10x4-M11 |
Response
{
"success": true,
"data": {
"shipping_method": "fedex",
"ship_to": {
"address_line_1": "123 Main St",
"address_line_2": "Apt 123",
"city": "Anytown",
"state": "NV",
"zip": "12345",
"country": "US"
},
"items": [
{
"parent_model": "AF12x12x1-M8",
"size": "12x12x1",
"actual_size": "11.5 x 11.5 x .75",
"quantity": 4,
"unit_price": 5.98,
"item_price": 23.92
},
{
"parent_model": "AF10x10x1A-M8",
"size": "10x10x1a",
"actual_size": "10 x 10 x .75",
"quantity": 10,
"unit_price": 8.58,
"item_price": 85.8
}
],
"total": {
"subtotal": 109.72,
"estimated_shipping_cost": 23.77,
"tax": {
"amount": 9.19,
"rate": 8.4,
"rate_type": "percent",
"jurisdiction": "NV"
},
"amount": 142.68,
"currency": "USD"
}
}
}
POST Create order
Create a new order in the system.
Note: A ship_to address is required to calculate shipping cost and sales tax. For customer_pick_up, pass your company address as the ship_to parameter. If your company has an approved tax-exempt document on file, no sales tax will be calculated.
POST /api/v1/orders
curl https://filterking.com/api/v1/orders \
-X POST \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: your-unique-key-here" \
-d '{
"test_order": false,
"po_number": "ABC3015",
"ship_to": {
"name": "Test Name",
"company": "Test company name",
"phone": "1234567890",
"address_line_1": "123 Main St",
"address_line_2": "Apt 123",
"city": "Anytown",
"state": "NY",
"zip": "12345",
"country": "US"
},
"ship_from": {
"name": "Filter King",
"company": "Filter King LLC",
"phone": "305-928-8910",
"address_line_1": "7301 NW 36th Ct",
"city": "Miami",
"state": "FL",
"zip": "33147",
"country": "US"
},
"shipping_method": "fedex",
"items": [
{
"parent_model": "AF10x10x1-M8",
"quantity": 40
},
{
"parent_model": "AF11.625x14.5x1A-M11",
"quantity": 10
}
]
}'
<?php
$response = $client->post('https://filterking.com/api/v1/orders', [
"headers" => [
"Authorization" => "Bearer " . $accessToken,
"Idempotency-Key" => "your-unique-key-here",
],
"json" => [
"test_order": false,
"po_number": "ABC3015",
"ship_to": {
"name": "Test Name",
"company": "Test company name",
"phone": "2152343245",
"address_line_1": "12300 Bermuda Rd",
"city": "Henderson",
"state": "NV",
"zip": "89044",
"country": "US"
},
"ship_from": {
"name": "Filter King",
"company": "Filter King LLC",
"phone": "305-928-8910",
"address_line_1": "7301 NW 36th Ct",
"city": "Miami",
"state": "FL",
"zip": "33147",
"country": "US"
},
"shipping_method": "fedex",
"items": [
{
"parent_model": "AF10x10x1-M8",
"quantity": 40
},
{
"parent_model": "AF11.625x14.5x1A-M11",
"quantity": 10
}
]
]
]);
$order = json_decode($response->getBody(), true);
const response = await fetch('https://filterking.com/api/v1/orders', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'your-unique-key-here',
},
body: JSON.stringify({
"test_order": false,
"po_number": "ABC3015",
"ship_to": {
"name": "Test Name",
"company": "Test company name",
"phone": "2152343245",
"address_line_1": "12300 Bermuda Rd",
"city": "Henderson",
"state": "NV",
"zip": "89044",
"country": "US"
},
"ship_from": {
"name": "Filter King",
"company": "Filter King LLC",
"phone": "305-928-8910",
"address_line_1": "7301 NW 36th Ct",
"city": "Miami",
"state": "FL",
"zip": "33147",
"country": "US"
},
"shipping_method": "fedex",
"items": [
{
"parent_model": "AF10x10x1-M8",
"quantity": 40
},
{
"parent_model": "AF11.625x14.5x1A-M11",
"quantity": 10
}
]
})
});
const order = await response.json();
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json',
'Idempotency-Key': 'your-unique-key-here'
}
data = {
"test_order": False,
"po_number": "ABC3015",
"ship_to": {
"name": "Test Name",
"company": "Test company name",
"phone": "2152343245",
"address_line_1": "12300 Bermuda Rd",
"city": "Henderson",
"state": "NV",
"zip": "89044",
"country": "US"
},
"ship_from": {
"name": "Filter King",
"company": "Filter King LLC",
"phone": "305-928-8910",
"address_line_1": "7301 NW 36th Ct",
"city": "Miami",
"state": "FL",
"zip": "33147",
"country": "US"
},
"shipping_method": "fedex",
"items": [
{
"parent_model": "AF10x10x1-M8",
"quantity": 40
},
{
"parent_model": "AF11.625x14.5x1A-M11",
"quantity": 10
}
]
}
response = requests.post(
'https://filterking.com/api/v1/orders',
headers=headers,
json=data
)
order = response.json()
Parameters
| Parameter | Type | Description | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
test_order |
boolean |
Set to false for production orders. Set to true for test orders, which will be automatically cancelled after the order is created. (required, default: false) |
||||||||||||||||||||||||||||||||||||
po_number |
string |
Purchase order number (optional, max: 30 characters. If not provided, default will be set as order_id) |
||||||||||||||||||||||||||||||||||||
ship_to |
object |
Shipping destination address (required)
Note: Please pass your company address if you choose shipping_method as
customer_pick_up
|
||||||||||||||||||||||||||||||||||||
ship_from |
object |
Shipping origin address (optional, default is FilterKing).
|
||||||||||||||||||||||||||||||||||||
shipping_method |
string |
Shipping carrier method: fedex or customer_pick_up |
||||||||||||||||||||||||||||||||||||
items |
array |
Array of filter items (required, min: 1, max: 100)
|
||||||||||||||||||||||||||||||||||||
Test Orders: Use test_order: true to place a test order. These orders will be automatically cancelled after creation. Use test_order: false for production orders.
Accepted MERV ratings: M8 (MERV 8), M11 (MERV 11), M13 (MERV 13), CO (Carbon)
Thickness: Only 0.5, 1, 2, or 4 inches are accepted.
Width and height measurements must use standard fractional increments. Accepted decimal values: .125 (1/8"), .25 (1/4"), .375 (3/8"), .5 (1/2"), .625 (5/8"), .75 (3/4"), or .875 (7/8").
Example: AF12.125x12.75x1-M8
For actual size filters, append A after thickness (e.g., AF12.125x12.75x1A-M8).
Response
{
"success": true,
"data": {
"order": {
"order_id": 12345678,
"po_number": "ABC3015",
"type": "wholesale",
"status": "pending",
"created_at": "2026-02-21T19:58:46.000000Z",
"items": [
{
"parent_model": "AF10x10x1-M8",
"size": "10x10x1",
"actual_size": "9.5 x 9.5 x .75",
"quantity": 40,
"unit_price": 2.84,
"item_price": 113.6
},
{
"parent_model": "AF11.625x14.5x1A-M11",
"size": "11.625x14.5x1a",
"actual_size": "11.625 x 14.5 x 0.75",
"quantity": 10,
"unit_price": 8.7,
"item_price": 87
}
],
"total": {
"subtotal": 200.6,
"shipping_cost": 12.66,
"tax": {
"amount": 17.86,
"rate": 8.4,
"rate_type": "percent",
"jurisdiction": "NV"
},
"amount": 231.12,
"currency": "USD"
}
}
},
"message": "Order created successfully"
}
GET List orders
Retrieve a paginated list of orders with optional filters.
GET /api/v1/orders
Request Examples
# Basic request - get all orders from last 24 hours
curl "https://filterking.com/api/v1/orders" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# With pagination and filters
curl "https://filterking.com/api/v1/orders?page=1&per_page=50&status=fulfilled" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# Get orders created after a specific date
curl "https://filterking.com/api/v1/orders?created_after=2026-02-21T19:58:46.000000Z" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# With sorting
curl "https://filterking.com/api/v1/orders?sort_by=created_at&sort_direction=desc" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
<?php
// Basic request
$response = $client->get('https://filterking.com/api/v1/orders', [
'headers' => [
'Authorization' => 'Bearer ' . $accessToken,
]
]);
// With query parameters
$response = $client->get('https://filterking.com/api/v1/orders', [
'headers' => [
'Authorization' => 'Bearer ' . $accessToken,
],
'query' => [
'page' => 1,
'per_page' => 50,
'status' => 'fulfilled',
'created_after' => '2026-02-21T19:58:46.000000Z',
'sort_by' => 'created_at',
'sort_direction' => 'desc'
]
]);
$orders = json_decode($response->getBody(), true);
// Basic request
const response = await fetch('https://filterking.com/api/v1/orders', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
// With query parameters
const params = new URLSearchParams({
page: 1,
per_page: 50,
status: 'fulfilled',
created_after: '2026-02-21T19:58:46.000000Z',
sort_by: 'created_at',
sort_direction: 'desc'
});
const response = await fetch(
`https://filterking.com/api/v1/orders?${params}`,
{
headers: {
'Authorization': `Bearer ${accessToken}`
}
}
);
const orders = await response.json();
headers = {
'Authorization': f'Bearer {access_token}'
}
# Basic request
response = requests.get(
'https://filterking.com/api/v1/orders',
headers=headers
)
# With query parameters
params = {
'page': 1,
'per_page': 50,
'status': 'fulfilled',
'created_after': '2026-02-21T19:58:46.000000Z',
'sort_by': 'created_at',
'sort_direction': 'desc'
}
response = requests.get(
'https://filterking.com/api/v1/orders',
headers=headers,
params=params
)
orders = response.json()
Query Parameters
| Parameter | Type | Description |
|---|---|---|
page |
integer |
Page number for pagination (default: 1, minimum: 1) |
per_page |
integer |
Number of items per page (default: 15, min: 1, max: 100) |
status |
string |
Filter by order status (e.g., pending, unfulfilled, fulfilled, cancelled) |
po_number |
string |
Filter by purchase order number (exact match, max: 30 characters) |
created_after |
string |
Filter orders created after this ISO 8601 date (UTC). If not provided, defaults to last 24 hours. |
updated_after |
string |
Filter orders updated after this ISO 8601 date (UTC). |
sort_by |
string |
Field to sort by (default: created_at, options: created_at, updated_at, amount, id) |
sort_direction |
string |
Sort direction (default: desc, options: asc, desc) |
Default Behavior: If no date filters (created_after or updated_after) are provided, the API returns orders from the last 24 hours. Use these parameters to retrieve older orders.
Response
{
"success": true,
"data": {
"orders": [
{
"order_id": 12345678,
"po_number": "ABC3015",
"type": "wholesale",
"status": "fulfilled",
"payment_status": "paid",
"ship_to": {
"name": "Test Name",
"company": "Test company name",
"phone": "1234567890",
"address_line_1": "123 Main St",
"address_line_2": "Apt 123",
"city": "Anytown",
"state": "NY",
"zip": "12345",
"country": "US"
},
"ship_from": {
"name": "FILTER KING",
"company": "FILTER KING LLC",
"phone": "877-570-9755",
"address_line_1": "7301 NW 36TH COURT",
"address_line_2": null,
"city": "MIAMI",
"state": "FL",
"zip": "33147",
"country": "US"
},
"shipping_method": "fedex",
"customer": {
"name": "Test Customer Name",
"email": "[email protected]",
"phone": "123-456-7890"
},
"items": [
{
"parent_model": "AF20x20x1-M8",
"size": "20x20x1",
"actual_size": "19.5 x 19.5 x .75",
"quantity": 35,
"unit_price": 9.42,
"item_price": 329.7
}
],
"shipments": [
{
"carrier": "fedex",
"service_type": "FEDEX_GROUND",
"tracking_number": "fedex_tracking_number",
"label_created_at": "2026-06-05T16:18:08.000000Z",
"estimated_delivery_date": "2026-06-08T15:14:53.000000Z",
"delivered_at": "2026-06-08T15:14:53.000000Z",
"tracking_status": {
"derived_status": "Delivered",
"derived_status_code": "DL",
"event_type": "DL",
"event_description": "Delivered",
"city": "delivered_city",
"state": "delivered_state",
"date": "2026-06-08T15:14:53.000000Z"
},
"shipment_contents": [
{
"parent_model": "AF20X30X1A-M13",
"quantity": 4
}
]
},
{
"carrier": "fedex",
"service_type": "FEDEX_GROUND",
"tracking_number": "fedex_tracking_number",
"label_created_at": "2026-06-05T14:27:32.000000Z",
"estimated_delivery_date": "2026-06-08T15:14:53.000000Z",
"delivered_at": "2026-06-08T15:14:53.000000Z",
"tracking_status": {
"derived_status": "Delivered",
"derived_status_code": "DL",
"event_type": "DL",
"event_description": "Delivered",
"city": "delivered_city",
"state": "delivered_state",
"date": "2026-06-08T15:14:53.000000Z"
},
"shipment_contents": [
{
"parent_model": "AF14X14X1A-M13",
"quantity": 4
}
]
}
],
"total": {
"subtotal": 200.6,
"shipping_cost": 12.66,
"tax": {
"amount": 17.86,
"rate": 8.4,
"rate_type": "percent",
"jurisdiction": "NV"
},
"amount": 231.12,
"currency": "USD"
},
"fulfilled_at": "2026-02-21T19:58:46.000000Z",
"created_at": "2026-02-21T19:58:46.000000Z",
"updated_at": "2026-02-21T19:58:46.000000Z"
}
],
"meta": {
"current_page": 1,
"per_page": 15,
"total": 42,
"last_page": 3,
"from": 1,
"to": 15
},
"links": {
"first": "https://filterking.com/api/v1/orders?page=1",
"last": "https://filterking.com/api/v1/orders?page=3",
"prev": null,
"next": "https://filterking.com/api/v1/orders?page=2"
}
}
}
GET Get order
Retrieve details of a specific order.
GET /api/v1/orders/{id}
URL Parameters
| Parameter | Type | Description |
|---|---|---|
id |
string |
The order ID |
Request
curl https://filterking.com/api/v1/orders/12345678 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
<?php
$response = $client->get('https://filterking.com/api/v1/orders/12345678', [
'headers' => [
'Authorization' => 'Bearer ' . $accessToken,
]
]);
$order = json_decode($response->getBody(), true);
const response = await fetch('https://filterking.com/api/v1/orders/12345678', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
const order = await response.json();
headers = {
'Authorization': f'Bearer {access_token}'
}
response = requests.get(
'https://filterking.com/api/v1/orders/12345678',
headers=headers
)
order = response.json()
Response
{
"success": true,
"data": {
"order": {
"order_id": 12345678,
"po_number": "ABC3015",
"type": "wholesale",
"status": "fulfilled",
"payment_status": "paid",
"ship_to": {
"name": "Test Name",
"company": "Test company name",
"phone": "1234567890",
"address_line_1": "123 Main St",
"address_line_2": "Apt 123",
"city": "Anytown",
"state": "NY",
"zip": "12345",
"country": "US"
},
"ship_from": {
"name": "FILTER KING",
"company": "FILTER KING LLC",
"phone": "877-570-9755",
"address_line_1": "7301 NW 36TH COURT",
"address_line_2": null,
"city": "MIAMI",
"state": "FL",
"zip": "33147",
"country": "US"
},
"shipping_method": "fedex",
"customer": {
"name": "Test Customer Name",
"email": "[email protected]",
"phone": "123-456-7890"
},
"items": [
{
"parent_model": "AF20x20x1-M8",
"size": "20x20x1",
"actual_size": "19.5 x 19.5 x .75",
"quantity": 35,
"unit_price": 9.42,
"item_price": 329.7
}
],
"shipments": [
{
"carrier": "fedex",
"service_type": "FEDEX_GROUND",
"tracking_number": "fedex_tracking_number",
"label_created_at": "2026-06-05T16:18:08.000000Z",
"estimated_delivery_date": "2026-06-08T15:14:53.000000Z",
"delivered_at": "2026-06-08T15:14:53.000000Z",
"tracking_status": {
"derived_status": "Delivered",
"derived_status_code": "DL",
"event_type": "DL",
"event_description": "Delivered",
"city": "delivered_city",
"state": "delivered_state",
"date": "2026-06-08T15:14:53.000000Z"
},
"shipment_contents": [
{
"parent_model": "AF20X30X1A-M13",
"quantity": 4
}
]
},
{
"carrier": "fedex",
"service_type": "FEDEX_GROUND",
"tracking_number": "fedex_tracking_number",
"label_created_at": "2026-06-05T14:27:32.000000Z",
"estimated_delivery_date": "2026-06-08T15:14:53.000000Z",
"delivered_at": "2026-06-08T15:14:53.000000Z",
"tracking_status": {
"derived_status": "Delivered",
"derived_status_code": "DL",
"event_type": "DL",
"event_description": "Delivered",
"city": "delivered_city",
"state": "delivered_state",
"date": "2026-06-08T15:14:53.000000Z"
},
"shipment_contents": [
{
"parent_model": "AF14X14X1A-M13",
"quantity": 4
}
]
}
],
"total": {
"subtotal": 200.6,
"shipping_cost": 12.66,
"tax": {
"amount": 17.86,
"rate": 8.4,
"rate_type": "percent",
"jurisdiction": "NV"
},
"amount": 231.12,
"currency": "USD"
},
"fulfilled_at": "2026-02-21T19:58:46.000000Z",
"created_at": "2026-02-21T19:58:46.000000Z",
"updated_at": "2026-02-21T19:58:46.000000Z",
}
}
}
GET Get order tracking
Retrieve details of a specific order shipment tracking.
GET /api/v1/orders/{id}/tracking
URL Parameters
| Parameter | Type | Description |
|---|---|---|
id |
string |
The order ID eg. 12345678 |
Request
curl https://filterking.com/api/v1/orders/12345678/tracking \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
<?php
$response = $client->get('https://filterking.com/api/v1/orders/12345678/tracking', [
'headers' => [
'Authorization' => 'Bearer ' . $accessToken,
]
]);
$order = json_decode($response->getBody(), true);
const response = await fetch('https://filterking.com/api/v1/orders/12345678/tracking', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
const order = await response.json();
headers = {
'Authorization': f'Bearer {access_token}'
}
response = requests.get(
'https://filterking.com/api/v1/orders/12345678/tracking',
headers=headers
)
order = response.json()
Response
{
"success": true,
"data": {
"order": {
"order_id": 12345678,
"type": "wholesale",
"status": "fulfilled",
"customer": {
"name": "Test Customer Name",
"email": "[email protected]",
"phone": "123-456-7890"
},
"shipments": [
{
"carrier": "fedex",
"service_type": "FEDEX_GROUND",
"tracking_number": "fedex_tracking_number",
"label_created_at": "2026-06-05T16:18:08.000000Z",
"estimated_delivery_date": "2026-06-08T15:14:53.000000Z",
"delivered_at": "2026-06-08T15:14:53.000000Z",
"tracking_status": {
"derived_status": "Delivered",
"derived_status_code": "DL",
"event_type": "DL",
"event_description": "Delivered",
"city": "delivered_city",
"state": "delivered_state",
"date": "2026-06-08T15:14:53.000000Z"
},
"shipment_contents": [
{
"parent_model": "AF20X30X1A-M13",
"quantity": 4
}
]
},
{
"carrier": "fedex",
"service_type": "FEDEX_GROUND",
"tracking_number": "fedex_tracking_number",
"label_created_at": "2026-06-05T14:27:32.000000Z",
"estimated_delivery_date": "2026-06-08T15:14:53.000000Z",
"delivered_at": "2026-06-08T15:14:53.000000Z",
"tracking_status": {
"derived_status": "Delivered",
"derived_status_code": "DL",
"event_type": "DL",
"event_description": "Delivered",
"city": "delivered_city",
"state": "delivered_state",
"date": "2026-06-08T15:14:53.000000Z"
},
"shipment_contents": [
{
"parent_model": "AF14X14X1A-M13",
"quantity": 4
}
]
}
],
"total": {
"subtotal": 200.6,
"shipping_cost": 12.66,
"tax": {
"amount": 17.86,
"rate": 8.4,
"rate_type": "percent",
"jurisdiction": "NV"
},
"amount": 231.12,
"currency": "USD"
},
"fulfilled_at": "2026-02-21T19:58:46.000000Z",
"created_at": "2026-02-21T19:58:46.000000Z",
"updated_at": "2026-02-21T19:58:46.000000Z",
}
}
}
POST Build custom filter
Build your custom filter and get parent model.
POST /api/v1/build-custom-filter
curl https://filterking.com/api/v1/build-custom-filter \
-X POST \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: your-unique-key-here" \
-d '{
"items": [
{
"size": "11.625x14.5x2a",
"merv": "M11",
"quantity": 2
}
]
}'
<?php
$client = new GuzzleHttp\Client();
$response = $client->post('https://filterking.com/api/v1/build-custom-filter', [
'headers' => [
'Authorization' => 'Bearer ' . $accessToken,
'Idempotency-Key' => 'your-unique-key-here',
],
'json' => [
'items' => [
[
'size' => '11.625x14.5x2a',
'merv' => 'M11',
'quantity' => 2
]
]
]
]);
$result = json_decode($response->getBody(), true);
const response = await fetch('https://filterking.com/api/v1/build-custom-filter', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'your-unique-key-here',
},
body: JSON.stringify({
"items": [
{
"size": "11.625x14.5x2a",
"merv": "M11",
"quantity": 2
}
]
})
});
const result = await response.json();
headers = {
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json',
'Idempotency-Key': 'your-unique-key-here'
}
data = {
"items": [
{
"size": "11.625x14.5x2a",
"merv": "M11",
"quantity": 2
}
]
}
response = requests.post(
'https://filterking.com/api/v1/build-custom-filter',
headers=headers,
json=data
)
result = response.json()
Parameters
| Parameter | Type | Description | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
items |
array |
Array of filter items (required, Minimum: 1 and Maximum: 10 items)
|
|||||||||||||||
Accepted MERV ratings: M8 (MERV 8), M11 (MERV 11), M13 (MERV 13), CO (Carbon)
Size format: {width}x{height}x{thickness} (e.g., 11.625x14.5x1)
Thickness: Only 0.5, 1, 2, or 4 inches are accepted.
Width and height measurements must use standard fractional increments. Accepted decimal values: .125 (1/8"), .25 (1/4"), .375 (3/8"), .5 (1/2"), .625 (5/8"), .75 (3/4"), or .875 (7/8").
Response
{
"success": true,
"data": {
"items": [
{
"parent_model": "AF11.625x14.5x2A-M11",
"size": "11.625x14.5x2a",
"actual_size": "11.625 x 14.5 x 1.75",
"quantity": 2,
"unit_price": 12.48,
"item_price": 24.96
}
]
}
}
Webhooks
Webhooks enable real-time notifications from Filter King to your application when specific events occur on your orders. Instead of repeatedly polling our API to check for status changes, you can configure webhook endpoints to receive automated push notifications as events happen.
How Webhooks Work
When you configure a webhook, you provide a URL endpoint on your server where Filter King will send HTTP POST requests whenever subscribed events occur. Each webhook contains detailed information about the event, allowing your application to react immediately without manual intervention.
Key Benefits: Webhooks eliminate the need for polling, reduce API usage, and ensure your system stays synchronized with order events in near real-time. Each webhook delivery attempt is logged, providing visibility into the success or failure of each notification.
Webhook Flow
- You configure a webhook endpoint URL in your Filter King dashboard
- Subscribe to specific events (e.g.,
order.shipped) - When the event occurs, Filter King sends a POST request to your endpoint
- Your server processes the payload and responds with a 2xx status code
- If delivery fails, Filter King retries with exponential backoff
Your Server Filter King
| |
| 1. Configure webhook endpoint |
|<--------------------------------------|
| |
| 2. Subscribe to events |
|<--------------------------------------|
| |
| 3. Order ships |
| |
| 4. Webhook notification (POST) |
|<--------------------------------------|
| |
| 5. Process payload |
| 6. Respond 200 OK |
|-------------------------------------->|
| |
| [Delivery confirmed] |
| |
Configuring Webhooks
Webhook configuration is managed through the Filter King dashboard. Navigate to API Management → Webhooks to add, edit, or remove your webhook endpoints.
Webhook Configuration Fields
| Field | Type | Description |
|---|---|---|
URL |
string |
The HTTPS endpoint where webhook events will be sent. Must be a publicly accessible URL. |
Events |
array |
List of event types to subscribe to. Currently supports order.shipped. |
Secret |
string |
Optional HMAC signing secret (minimum 16 characters). Used to verify webhook authenticity. |
Active |
boolean |
Toggle webhook on/off without deleting configuration. Default: true. |
URL Requirements: Your webhook endpoint must be accessible over HTTPS and respond within 10 seconds. While testing, you can use tools like webhook.site or ngrok to create temporary public endpoints.
Implementation Example: Webhook Handler
<?php
// webhook-handler.php
// Get the raw POST data
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
// Your webhook secret from dashboard
$webhookSecret = 'your_webhook_secret_here';
// Verify signature if secret is configured
if (!empty($webhookSecret)) {
$expectedSignature = hash_hmac('sha256', $payload, $webhookSecret);
if (!hash_equals($expectedSignature, $signature)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
}
// Parse the payload
$event = json_decode($payload, true);
if (!$event) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON']);
exit;
}
// Process based on event type
$eventType = $event['event'] ?? '';
switch ($eventType) {
case 'order.shipped':
handleOrderShipped($event);
break;
default:
// Log unknown event types
error_log("Unknown event type: $eventType");
}
// Respond with 200 OK to acknowledge receipt
http_response_code(200);
echo json_encode(['success' => true]);
function handleOrderShipped($event)
{
$orderId = $event['data']['order_id'] ?? null;
$poNumber = $event['data']['po_number'] ?? null;
$status = $event['data']['status'] ?? null;
// Update your local database
// updateOrderStatus($orderId, $status);
// Process shipment details
foreach ($event['data']['shipments'] ?? [] as $shipment) {
$trackingNumber = $shipment['tracking_number'];
$carrier = $shipment['carrier'];
// Store tracking information
// storeTrackingInfo($orderId, $trackingNumber, $carrier);
}
// Trigger any business logic (notifications, inventory updates, etc.)
// triggerShippedNotifications($orderId, $poNumber);
}
// webhook-handler.js (Express.js)
const express = require('express');
const crypto = require('crypto');
const app = express();
// Your webhook secret from dashboard
const WEBHOOK_SECRET = 'your_webhook_secret_here';
app.post('/webhooks/filterking', express.raw({ type: 'application/json' }), (req, res) => {
const payload = req.body;
const signature = req.get('X-Webhook-Signature') || '';
// Verify signature if secret is configured
if (WEBHOOK_SECRET) {
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex');
if (signature !== expectedSignature) {
console.error('Invalid webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
}
// Parse the event
let event;
try {
event = JSON.parse(payload.toString());
} catch (err) {
console.error('Invalid JSON payload');
return res.status(400).json({ error: 'Invalid JSON' });
}
// Process based on event type
const eventType = event.event;
switch (eventType) {
case 'order.shipped':
handleOrderShipped(event);
break;
default:
console.log(`Unknown event type: ${eventType}`);
}
// Respond with 200 OK to acknowledge receipt
res.status(200).json({ success: true });
});
function handleOrderShipped(event) {
const { order_id, po_number, status, shipments } = event.data;
console.log(`Order ${order_id} (${po_number}) has been ${status}`);
// Update your local database
// await updateOrderStatus(order_id, status);
// Process shipment details
shipments.forEach(shipment => {
const { tracking_number, carrier } = shipment;
console.log(`Tracking: ${carrier} - ${tracking_number}`);
// Store tracking information
// await storeTrackingInfo(order_id, tracking_number, carrier);
});
// Trigger any business logic
// await triggerShippedNotifications(order_id, po_number);
}
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Webhook server listening on port ${PORT}`);
});
# webhook_handler.py (Flask)
from flask import Flask, request, jsonify
import hmac
import hashlib
app = Flask(__name__)
# Your webhook secret from dashboard
WEBHOOK_SECRET = 'your_webhook_secret_here'
@app.route('/webhooks/filterking', methods=['POST'])
def handle_webhook():
payload = request.get_data(as_text=False)
signature = request.headers.get('X-Webhook-Signature', '')
# Verify signature if secret is configured
if WEBHOOK_SECRET:
expected_signature = hmac.new(
WEBHOOK_SECRET.encode(),
payload,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected_signature, signature):
print('Invalid webhook signature')
return jsonify({'error': 'Invalid signature'}), 401
# Parse the event
try:
event = request.get_json()
except:
print('Invalid JSON payload')
return jsonify({'error': 'Invalid JSON'}), 400
if not hmac.compare_digest(expected_signature, received_signature):
print('Invalid webhook signature')
return jsonify({'error': 'Invalid signature'}), 401
# Process based on event type
event_type = event.get('event')
if event_type == 'order.shipped':
handle_order_shipped(event)
else:
print(f'Unknown event type: {event_type}')
# Respond with 200 OK to acknowledge receipt
return jsonify({'success': True}), 200
def handle_order_shipped(event):
data = event.get('data', {})
order_id = data.get('order_id')
po_number = data.get('po_number')
status = data.get('status')
shipments = data.get('shipments', [])
print(f'Order {order_id} ({po_number}) has been {status}')
# Update your local database
# update_order_status(order_id, status)
# Process shipment details
for shipment in shipments:
tracking_number = shipment.get('tracking_number')
carrier = shipment.get('carrier')
print(f'Tracking: {carrier} - {tracking_number}')
# Store tracking information
# store_tracking_info(order_id, tracking_number, carrier)
# Trigger any business logic
# trigger_shipped_notifications(order_id, po_number)
if __name__ == '__main__':
app.run(port=3000)
Webhook Events
Filter King currently supports the following webhook events. More events will be added in the future.
order.shipped
Triggered when an order's shipping label has been created and tracking information is available.
This event fires when the order status changes to partially shipped or fulfilled.
Important: The order.shipped webhook contains comprehensive shipment details including
carrier information, tracking numbers, estimated delivery dates, and the complete contents of each shipment.
Event Payload Structure
{
"id": "evt_ofjkTd4d1MZ8ODBs4G20cR06xChsrarB",
"event": "order.shipped",
"data": {
"order_id": 12345678,
"po_number": "AB332454",
"status": "fulfilled",
"created_at": "2026-07-27T18:45:05.000000Z",
"shipped_at": "2026-07-27T23:05:06.000000Z",
"shipments": [
{
"carrier": "fedex",
"service_type": "GROUND_HOME_DELIVERY",
"tracking_number": "874945190000",
"label_created_at": "2026-07-27T19:05:06.072721Z",
"estimated_delivery_date": "2026-07-29T00:00:00.000000Z",
"delivered_at": "2026-08-13T18:35:06.266918Z",
"tracking_status": {
"city": null,
"date": "2026-07-27T19:07:00.000000Z",
"derived_status": "Label created",
"derived_status_code": "IN",
"event_description": "Shipment information sent to FedEx",
"event_type": "OC",
"state": null
},
"shipment_contents": [
{
"parent_model": "AF20x25x1-M11",
"quantity": 4
},
{
"parent_model": "AF20x20x1-M11",
"quantity": 4
}
]
}
]
},
"created_at": "2026-08-13T18:35:06.264647Z"
}
Payload Field Descriptions
| Field | Type | Description |
|---|---|---|
id |
string |
Unique webhook event ID (prefixed with evt_) |
event |
string |
Event type that triggered this webhook |
data |
object |
Event-specific data payload |
data.order_id |
integer |
Filter King internal order ID |
data.po_number |
string |
Purchase order number (if provided during order creation) |
data.status |
string |
Current order status |
data.created_at |
string |
Order creation timestamp (ISO 8601 UTC) |
data.shipped_at |
string |
Shipment timestamp (ISO 8601 UTC) |
data.shipments |
array |
List of shipments for this order (may contain multiple shipments) |
shipments[].carrier |
string |
Shipping carrier (e.g., fedex) |
shipments[].service_type |
string |
Carrier service type (e.g., GROUND_HOME_DELIVERY, FEDEX_GROUND) |
shipments[].tracking_number |
string |
Carrier tracking number for the shipment |
shipments[].label_created_at |
string |
Shipping label creation timestamp (ISO 8601 UTC) |
shipments[].estimated_delivery_date |
string |
Carrier's estimated delivery date (ISO 8601 UTC) |
shipments[].delivered_at |
string |
Actual delivery timestamp (ISO 8601 UTC), null if not yet delivered |
shipments[].tracking_status |
object |
Latest tracking status information from the carrier |
tracking_status.derived_status |
string |
Human-readable status (e.g., "Label created", "In Transit", "Delivered") |
tracking_status.derived_status_code |
string |
Two-letter status code (e.g., IN, IT, DL) |
tracking_status.event_type |
string |
Carrier-specific event type code |
tracking_status.event_description |
string |
Detailed description of the tracking event |
tracking_status.city |
string |
City where the tracking event occurred (if applicable) |
tracking_status.state |
string |
State where the tracking event occurred (if applicable) |
shipments[].shipment_contents |
array |
List of filter items included in this shipment |
shipment_contents[].parent_model |
string |
Filter parent model (e.g., AF20x25x1-M11) |
shipment_contents[].quantity |
integer |
Quantity of this item in the shipment |
created_at |
string |
Webhook event creation timestamp (ISO 8601 UTC) |
Signature Header: If you configured a webhook secret, the X-Webhook-Signature HTTP header
will contain the HMAC-SHA256 signature. See the Signature Verification section for details.
Signature Verification
To ensure webhook authenticity and prevent fraudulent requests, Filter King supports HMAC-SHA256 signature verification.
When you configure a webhook secret in your dashboard, each webhook request includes an X-Webhook-Signature
HTTP header containing the signature that you can use to verify the request originated from Filter King.
How Signature Verification Works
- When configuring your webhook, generate and securely store a secret (minimum 16 characters)
- Filter King computes an HMAC-SHA256 hash of the JSON payload using your secret
- The signature is sent in the
X-Webhook-SignatureHTTP header - Your server reads the header and recomputes the HMAC using the same secret
- If signatures match, the webhook is authentic; otherwise, reject the request with a 401 status
Computing the Signature
The signature is computed as:
signature = HMAC-SHA256(raw_json_payload, webhook_secret)
<?php
$payload = file_get_contents('php://input');
$webhookSecret = 'your_webhook_secret';
$receivedSignature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$expectedSignature = hash_hmac('sha256', $payload, $webhookSecret);
// Use hash_equals to prevent timing attacks
if (hash_equals($expectedSignature, $receivedSignature)) {
// Signature is valid, process the webhook
$event = json_decode($payload, true);
} else {
// Invalid signature, reject with 401
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(payload);
const expectedSignature = hmac.digest('hex');
// Use timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signature)
);
}
// Usage
const payload = req.body; // Or raw body if using express.raw()
const signature = req.get('X-Webhook-Signature');
const secret = 'your_webhook_secret';
if (verifySignature(payload, signature, secret)) {
// Signature is valid, process the webhook
} else {
// Invalid signature, reject with 401
res.status(401).send('Invalid signature');
}
import hmac
import hashlib
def verify_signature(payload, signature, secret):
expected_signature = hmac.new(
secret.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
# Use hmac.compare_digest to prevent timing attacks
return hmac.compare_digest(expected_signature, signature)
# Usage
payload = request.get_data(as_text=False)
signature = request.headers.get('X-Webhook-Signature', '')
secret = 'your_webhook_secret'
if verify_signature(payload.decode(), signature, secret):
# Signature is valid, process the webhook
event = request.get_json()
else:
# Invalid signature, reject with 401
return jsonify({'error': 'Invalid signature'}), 401
Security Best Practice: Always use timing-safe comparison functions when verifying signatures to prevent
timing attacks. In PHP, use hash_equals(); in Node.js, use crypto.timingSafeEqual();
in Python, use hmac.compare_digest().
Retry Logic
If your webhook endpoint fails to respond or returns a non-2xx status code, Filter King will automatically retry delivery with exponential backoff. This ensures temporary network issues or server downtime don't result in lost events.
Retry Configuration
| Setting | Value |
|---|---|
| Maximum Attempts | 3 attempts (initial + 2 retries) |
| Request Timeout | 10 seconds per attempt |
| Retry Delays | 60s, 120s, 180s (exponential backoff) |
Retry Behavior
- First attempt: Immediate delivery
- Second attempt: 60 seconds after first failure
- Third attempt: 120 seconds after second failure
- Final attempt: 180 seconds after third failure
- After 3 failed attempts: Webhook is marked as permanently failed
Responding to Webhooks: Always respond with a 2xx status code as quickly as possible. If you need to perform time-consuming processing, acknowledge the webhook immediately and process the payload asynchronously in the background.
Delivery Logs
All webhook delivery attempts are logged in the Filter King dashboard. You can view detailed logs including:
- Number of attempts made
- HTTP status code received
- Response body (truncated to 1000 characters)
- Timestamps of each attempt
- Final delivery status
Best Practices
1. Quick Acknowledgment
Respond to webhooks as quickly as possible, ideally within 1-2 seconds. If processing requires significant time, acknowledge immediately (200 OK) and handle the payload asynchronously:
<?php
// Validate and acknowledge immediately
$payload = file_get_contents('php://input');
$event = json_decode($payload, true);
if ($event) {
// Queue for background processing
enqueueWebhookForProcessing($event);
// Respond immediately
http_response_code(200);
echo json_encode(['success' => true]);
}
app.post('/webhooks', (req, res) => {
const event = req.body;
// Queue for background processing
queueJob('process-webhook', event);
// Respond immediately
res.status(200).json({ success: true });
});
2. Idempotent Processing
Webhooks may be delivered multiple times (retries or duplicate events). Design your webhook handler to be idempotent:
- Check if the event has already been processed using the unique event ID
- Use database transactions or unique constraints to prevent duplicate operations
- Make processing operations safe to run multiple times
<?php
$eventId = $event['id'];
// Check if already processed
if (webhookAlreadyProcessed($eventId)) {
http_response_code(200);
echo json_encode(['success' => true, 'message' => 'Already processed']);
exit;
}
// Process the webhook
processOrderShipped($event);
// Mark as processed
markWebhookAsProcessed($eventId);
http_response_code(200);
echo json_encode(['success' => true]);
const eventId = event.id;
// Check if already processed
const alreadyProcessed = await checkProcessedEvent(eventId);
if (alreadyProcessed) {
return res.status(200).json({
success: true,
message: 'Already processed'
});
}
// Process the webhook
await processOrderShipped(event);
// Mark as processed
await markEventProcessed(eventId);
res.status(200).json({ success: true });
3. Secure Your Endpoint
- Always use HTTPS endpoints in production
- Configure and verify webhook signatures
- Consider implementing IP whitelisting if needed
- Validate the payload structure before processing
4. Error Handling
- Log all received webhooks for debugging
- Log processing failures separately from delivery failures
- Implement alerts for repeated delivery failures
- Monitor webhook delivery logs in the Filter King dashboard
5. Testing
- Test webhook handlers locally using tools like ngrok or webhook.site
- Create test orders to verify webhook delivery
- Test signature verification with your configured secret
- Test failure scenarios (invalid signature, malformed JSON, etc.)
Pro Tip: Use the Filter King dashboard to view webhook delivery logs and retry failed webhooks manually if needed. You can also test your webhook endpoint with sample payload data from the dashboard.