filestack
Official Filestack plugin — upload files, build transformations, and generate security policies directly from Cursor.
cursor.directory·↓ 6
规则
filestack-transform
Build a Filestack CDN transformation URL from a file handle and a plain-English description
# /filestack-transform
Build a Filestack CDN transformation URL from a file handle (or existing CDN URL) and a
plain-English description of what you want to do.
## How to use this command
1. Parse the first argument as the file handle or CDN URL
- Bare handle: `abc123XYZ`
- Full URL: `https://cdn.filestackcontent.com/abc123XYZ`
2. Interpret the rest of the arguments as a plain-English transformation description
3. Map to Filestack CDN URL syntax and return the full URL with an explanation of each segment
## CDN URL format
~~~
https://cdn.filestackcontent.com/<operation1>=<param1>:<val1>,<param2>:<val2>/<operation2>/<handle>
~~~
## Transform reference
| What you say | CDN segment |
| --- | --- |
| resize to 800x600 | `resize=width:800,height:600` |
| resize width 400 | `resize=width:400` |
| crop to [50,50,200,200] | `crop=dim:[50,50,200,200]` |
| detect and crop face | `crop_faces=faces:1` |
| rotate 90 degrees | `rotate=deg:90` |
| flip vertically | `flip` |
| flip horizontally | `flop` |
| enhance | `enhance` |
| grayscale / monochrome | `monochrome` |
| convert to webp | `output=format:webp` |
| convert to jpg quality 85 | `output=format:jpg,quality:85` |
| blur (amount 5) | `blur=amount:5` |
| sharpen | `sharpen=amount:3` |
## Examples
~~~
/filestack-transform abc123XYZ resize to 800x600 and convert to webp
→ https://cdn.filestackcontent.com/resize=width:800,height:600/output=format:webp/abc123XYZ
/filestack-transform abc123XYZ detect face, enhance, and convert to jpg at 85 quality
→ https://cdn.filestackcontent.com/crop_faces=faces:1/enhance/output=format:jpg,quality:85/abc123XYZ
/filestack-transform https://cdn.filestackcontent.com/abc123XYZ rotate 90 and monochrome
→ https://cdn.filestackcontent.com/rotate=deg:90/monochrome/abc123XYZ
~~~
If called with no arguments or an invalid handle, output a usage hint and examples — do not error.MCP
filestack
MCP server: filestack
{
"command": "node",
"args": [
"${CLAUDE_PLUGIN_ROOT}/mcp/dist/bundle.js"
],
"env": {
"FILESTACK_API_KEY": "${FILESTACK_API_KEY:-APQLlwqrRScGxhw78gs9Wz}",
"FILESTACK_APP_SECRET": "${FILESTACK_APP_SECRET:-}"
}
}Skill
filestack-error-diagnosis
>
# Filestack Error Diagnosis
## Error Response Shape
Filestack APIs return errors in two formats:
**v1 API format** (`api.filestackapi.com`):
```json
{ "result": "error", "error": { "code": 403, "msg": "Policy required" } }
```
**Processing/CDN format** (`process.filestackapi.com`, `cdn.filestackcontent.com`):
```json
{ "error": "Unsupported conversion", "result": null }
```
## HTTP Error Code Reference
| Code | Meaning | Likely Cause | Fix |
| --- | --- | --- | --- |
| 401 | Unauthorized | Invalid or missing API key | Check `FILESTACK_API_KEY` is correct and hasn't been rotated |
| 403 | Forbidden | Security policy violation, or CORS origin not whitelisted | See policy errors below |
| 404 | Not Found | Handle doesn't exist, has been deleted, or has expired | Verify handle is correct; check app's handle expiry setting |
| 429 | Rate Limited | Too many requests | Respect `Retry-After` header; implement exponential backoff |
| 500/502/503 | Server Error | Transient Filestack service issue | Retry with backoff; check status.filestack.com |
## Security Policy Errors (403)
403s from security-enabled apps are usually one of:
1. **Expired policy** — `expiry` unix timestamp is in the past
```python
import time
policy = { "expiry": int(time.time()) + 3600, "call": ["read"] } # 1 hour from now
```
2. **Wrong signature** — HMAC computed with wrong secret, or policy was modified after signing
- Re-generate both policy and signature together — never modify the policy JSON after signing
3. **Insufficient `call` scope** — policy doesn't include the operation being performed
- `read` for CDN delivery, `store` for upload, `convert` for transformations, `remove` for delete
4. **Wrong CORS origin** — browser request blocked
- Filestack dashboard → your app → Security → add your domain
## Transformation Errors
| Error | Cause | Fix |
| --- | --- | --- |
| "Unsupported input format" | Input file format not supported by the transform | Check Filestack docs for supported input formats per transform |
| "Output size limit exceeded" | Result would be larger than the app's output limit | Reduce target dimensions or use progressive encoding |
| "Processing timeout" | Transform took too long (usually video) | Use async transforms with a callback URL for video |
| "Invalid parameter" | Wrong parameter name or value type | Check CDN URL syntax: `param:value` (colon, not `=`) |
## Diagnosis Checklist
Run through this in order when debugging Filestack errors:
1. **API key valid?** — Try a simple retrieve call: `GET https://www.filestackapi.com/api/file/<handle>/metadata?key=<apikey>`
2. **Policy not expired?** — Decode the base64 policy and check the `expiry` field is in the future
3. **Handle exists?** — Retrieve metadata for the handle; 404 means handle is gone
4. **CORS origin whitelisted?** — Check Filestack dashboard → app → Security → Domains
5. **Transform params valid?** — Run the CDN URL in a browser; the error is often self-describing
6. **Rate limit hit?** — Check for 429 and `Retry-After` header in your HTTP client logsSkill
filestack-sdk-integration
>
# Filestack SDK Integration
## Agent Behavior
When the user asks you to create a new app or page that uses Filestack:
1. **Scaffold in the current directory** — do NOT create a new subdirectory. Use `npm create vite@latest . -- --template react` (note the `.`) or write files directly into the working directory. If the directory is not empty, ask first.
2. **Use the demo API key** — embed `APQLlwqrRScGxhw78gs9Wz` directly in the generated code with a comment that it is a demo key. Never use `'YOUR_API_KEY'` as a placeholder — it forces the user to do a manual find-and-replace before they can test.
3. **Install dependencies and start the dev server** — after generating the code, run `npm install` and `npm run dev` so the user can see the result immediately. Do not tell the user to run these commands manually.
## Initialization
**JavaScript / TypeScript**
```js
import * as filestack from 'filestack-js';
const API_KEY = 'APQLlwqrRScGxhw78gs9Wz'; // Demo key — get your own at https://dev.filestack.com/signup/free/
const client = filestack.init(API_KEY);
```
**Via CDN loader (no bundler)**
```html
<script src="https://static.filestackapi.com/filestack-js/3.x.x/filestack.min.js"></script>
<script>
const API_KEY = 'APQLlwqrRScGxhw78gs9Wz'; // Demo key — get your own at https://dev.filestack.com/signup/free/
const client = filestack.init(API_KEY);
</script>
```
**Python**
```python
from filestack import Client
API_KEY = 'APQLlwqrRScGxhw78gs9Wz' # Demo key — get your own at https://dev.filestack.com/signup/free/
client = Client(API_KEY)
```
## File Upload
```js
// Direct upload (returns a promise)
const result = await client.upload('/path/to/file.jpg');
// result: { handle, url, filename, size, mimetype }
```
## Picker Widget
```js
const picker = client.picker({
// Accept specific file types
accept: ['image/*', 'application/pdf'],
maxFiles: 5,
maxSize: 10 * 1024 * 1024, // 10 MB
// Cloud sources (uses filestack-dialog OAuth)
// Available sources: local_file_system, url, imagesearch, webcam, video, audio,
// googledrive, dropbox, facebook, instagram, box, onedrive, onedriveforbusiness,
// gmail, github, googlephotos, unsplash, tint, customsource
fromSources: ['local_file_system', 'googledrive', 'dropbox', 'url'],
// Where to store uploaded files
storeTo: {
location: 's3',
path: '/uploads/',
access: 'public',
},
onUploadDone: (result) => {
console.log(result.filesUploaded); // Array of FileResult objects
},
});
picker.open();
```
## Upload Response Shape
```typescript
interface FileResult {
handle: string; // Unique file identifier (use this for transforms, CDN URLs)
url: string; // https://cdn.filestackcontent.com/<handle>
filename: string;
size: number; // bytes
mimetype: string; // e.g. "image/jpeg"
source: string; // "local_file_system" | "googledrive" | etc.
}
```
## Common Mistakes
1. **Wrong API key scope** — if you see 403 errors, verify your API key in the Filestack dashboard
has the necessary permissions for the operations you're calling.
2. **Missing CORS whitelist** — go to Filestack dashboard → your app → Security → add your
domain to the allowed origins list. Without this, picker won't open from localhost or
production domains.
3. **Transform chain ordering** — transforms are applied left-to-right in the CDN URL.
Put `resize` before `output` (format conversion), not after.
4. **Not awaiting upload** — `client.upload()` returns a Promise. Always `await` it or
use `.then()`.
## Framework Patterns
**React**
```jsx
import { useState } from 'react';
import * as filestack from 'filestack-js';
const API_KEY = 'APQLlwqrRScGxhw78gs9Wz'; // Demo key — get your own at https://dev.filestack.com/signup/free/
const client = filestack.init(API_KEY);
export function FileUploader({ onUpload }) {
const [uploading, setUploading] = useState(false);
const openPicker = () => {
client.picker({
onUploadDone: (res) => onUpload(res.filesUploaded),
}).open();
};
return <button onClick={openPicker}>Upload file</button>;
}
```
**Next.js (server-side upload)**
```typescript
// pages/api/upload.ts
import { init } from 'filestack-js';
const API_KEY = process.env.FILESTACK_API_KEY || 'APQLlwqrRScGxhw78gs9Wz'; // Demo fallback
export default async function handler(req, res) {
const client = init(API_KEY);
// URL-based store (no file data needed server-side)
const result = await client.storeURL(req.body.url);
res.json(result);
}
```Skill
filestack-webhook-setup
>
# Filestack Webhook Setup
## Webhook Event Types
| Event | When it fires |
| --- | --- |
| `fp.upload` | File successfully uploaded and stored |
| `fp.converse` | Image conversion (filepicker-converse) completed |
| `fp.delete` | File deleted |
| `fp.overwrite` | File overwritten |
| `fp.video_converse` | Video conversion completed |
| `fp.scan` | Antivirus scan completed |
| `fp.export` | File exported to cloud storage |
| `fs.workflow` | Workflow pipeline completed |
## Register a Webhook URL
```bash
curl -X POST https://api.filestackapi.com/webhooks/YOUR_APP_ID \
-H "Content-Type: application/json" \
-H "Filestack-API-Key: YOUR_API_KEY" \
-d '{ "url": "https://yourdomain.com/filestack-webhook", "webhook_type": "fp.upload" }'
```
## Webhook Payload Shape
```json
{
"id": 30813791,
"action": "fp.upload",
"timestamp": 1710000000,
"text": {
"container": "filestack-uploads",
"url": "https://cdn.filestackcontent.com/abc123XYZ",
"filename": "image.jpg",
"client": "Computer",
"key": "abc123XYZ",
"type": "image/jpeg",
"status": "Stored",
"size": 102400
}
}
```
## Signature Verification
Filestack signs webhooks with HMAC-SHA256 using a **per-webhook secret** (created in the
Developer Portal when configuring the webhook — this is NOT the app secret). The signature covers
`"{timestamp}.{body}"` — the `FS-Timestamp` header value, a dot, and the raw request body.
**Headers sent by Filestack:**
- `FS-Signature` — HMAC-SHA256 hex digest
- `FS-Timestamp` — Unix timestamp (string) used in signing
**Node.js**
```typescript
import { createHmac } from 'crypto';
import type { Request, Response } from 'express';
export function verifyFilestackSignature(req: Request): boolean {
const signature = req.headers['fs-signature'] as string;
const timestamp = req.headers['fs-timestamp'] as string;
if (!signature || !timestamp) return false;
// Filestack signs "{timestamp}.{body}" — must match exactly
const signPayload = `${timestamp}.${req.rawBody}`;
const expected = createHmac('sha256', process.env.FILESTACK_WEBHOOK_SECRET!)
.update(signPayload)
.digest('hex');
return signature === expected;
}
export function webhookHandler(req: Request, res: Response): void {
// CRITICAL: Return 200 BEFORE processing — prevents duplicate delivery on timeout
if (!verifyFilestackSignature(req)) {
res.status(401).send('Invalid signature');
return;
}
res.status(200).send('ok');
// Process asynchronously after responding
setImmediate(() => processEvent(req.body));
}
```
**Python (Flask)**
```python
import hmac, hashlib, os, threading
from flask import request, jsonify
def verify_signature(raw_body: bytes, timestamp: str, signature: str) -> bool:
# Filestack signs "{timestamp}.{body}"
sign_payload = f"{timestamp}.{raw_body.decode('latin-1')}"
expected = hmac.new(
os.environ['FILESTACK_WEBHOOK_SECRET'].encode('latin-1'),
sign_payload.encode('latin-1'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route('/filestack-webhook', methods=['POST'])
def webhook():
sig = request.headers.get('FS-Signature', '')
ts = request.headers.get('FS-Timestamp', '')
if not verify_signature(request.get_data(), ts, sig):
return jsonify({'error': 'invalid signature'}), 401
# Acknowledge immediately before any slow processing
threading.Thread(target=process_event, args=(request.json,)).start()
return jsonify({'status': 'ok'}), 200
```
**Go**
```go
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
)
func verifySignature(body []byte, timestamp, signature, appSecret string) bool {
// Filestack signs "{timestamp}.{body}"
signPayload := fmt.Sprintf("%s.%s", timestamp, string(body))
mac := hmac.New(sha256.New, []byte(appSecret))
mac.Write([]byte(signPayload))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
```
## Critical Patterns
**Return 200 immediately, then process async.**
Filestack's webhook consumer has a configurable timeout (default ~30s). If your handler times out,
Filestack retries — leading to duplicate processing. Always acknowledge (200/201/204) before doing any slow work.
**Handlers must be idempotent.**
Filestack retries failed deliveries up to 5 times with timeouts [10s, 60s, 5min, 15min, 1hr].
Success codes are 200, 201, 204. Use the `id` field from the payload as an idempotency key.
**Use raw body for signature verification.**
JSON parsers may reformat the body, invalidating the signature. Capture the raw bytes
before parsing:
```js
// Express: use express.raw() for the webhook route
app.post('/filestack-webhook', express.raw({ type: 'application/json' }), webhookHandler);
```