VPS / Bare Metal
How: Run API Hub directly on your own VM or machine.
Setup: Use Node 20+, run via systemd/PM2, terminate TLS with Nginx/Caddy, and keep env vars + logs managed on host.
API Hub is a standalone Node.js service for teams whose language or platform does not yet have a native Fynlink SDK.
Because of Fynlink's end-to-end encrypted architecture, a universal direct REST surface is not offered for every client environment. API Hub bridges that gap by exposing clean REST endpoints while executing official SDK flows internally, so you get consistent behavior, security, and responses across stacks.
If your language of choice does not have an official SDK, API Hub gives you a production-ready fallback. Integrate once over REST and keep the same core behavior expected from official client flows.
Language-agnostic integration
Call API Hub from Python, PHP, Go, Java, shell, or no-code tools with the same REST contract.
Single integration point
Keep API traffic centralized behind one proxy service instead of duplicating client logic across apps.
Architecture-compatible
API Hub executes official SDK flows internally instead of reverse-engineered direct backend calls.
API Hub is intentionally thin and proxy-oriented. It does not replace upstream business logic.
Step 1
Client sends REST request
Any language or tool can call API Hub over HTTP.
Step 2
API Hub processes request
Auth, schema validation, routing, and request/response shaping happen here.
Step 3
Fynlink API executes upstream logic
Official SDK flow is used and normalized JSON is returned to the caller.
Use the official prebuilt Docker image directly.
docker pull ghcr.io/fynlink/api-hub:latestcurl -fsSL https://raw.githubusercontent.com/FynLink/api-hub/main/.env.example -o .env
docker run -d --name fyn-api-hub \ --env-file .env \ -p 8787:8787 \ --restart unless-stopped \ ghcr.io/fynlink/api-hub:latest
API Hub includes interactive Swagger docs out of the box. Use the UI for manual testing and use the OpenAPI JSON for client generation and tooling integration.
| URL | Purpose |
|---|---|
http://localhost:8787/health | Liveness check endpoint. |
http://localhost:8787/openapi.json | OpenAPI 3.0 document for all API Hub routes. |
http://localhost:8787/docs | Built-in Swagger UI, powered by OpenAPI. |
http://localhost:8787/v1 | Endpoint index summary from API Hub. |
http://localhost:8787/v1/info | Team/account bootstrap payload (optionally force refresh). |
Swagger and OpenAPI endpoints are available immediately after startup.
For AI assistants and agent platforms, MCP is the right integration model. It gives structured tools, clear permission boundaries, and predictable tool-calling behavior.
Use the dedicated MCP guide for end-to-end setup:
If your AI platform supports OpenAPI/Swagger tool import but not MCP, API Hub can still be used effectively because it exposes an OpenAPI document at /openapi.json.
| Setup Item | What to provide |
|---|---|
Schema URL | https://api-hub.example.com/openapi.json |
Base URL | https://api-hub.example.com |
Auth header | Authorization: Bearer <API_HUB_BEARER_TOKEN> |
Docs UI (optional) | https://api-hub.example.com/docs |
{ "openapi": "https://api-hub.example.com/openapi.json", "server": "https://api-hub.example.com", "headers": { "Authorization": "Bearer <API_HUB_BEARER_TOKEN>" } }
Keep token scope minimal and expose API Hub only over HTTPS in production.
These are common AI tasks mapped to API Hub operations. Keep this section conceptual and use the full request examples in the REST API section below.
| AI Task | Endpoint | Method |
|---|---|---|
Create campaign short link | /v1/links | POST |
Fetch 30-day performance trend | /v1/analytics/links/:id/timeseries | GET |
Verify branded domain setup | /v1/domains/:id/verify | POST |
REST API:Links, Domains, and Analytics sections.| Variable | Description |
|---|---|
PORT | Port used by API Hub. Default: 8787 |
NODE_ENV | Runtime mode: development, test, production |
API_HUB_BEARER_TOKEN | Optional token for caller authentication |
API_HUB_CORS_ORIGIN | CORS origin. Default: * |
| Variable | Description |
|---|---|
FYN_API_TOKEN | Required upstream API token used by @fynlink/sdk |
FYN_SECRET_KEY | Required for encrypted read/list/update flows |
FYN_API_URL | Optional upstream base URL |
FYN_TIMEOUT_MS | Upstream timeout in milliseconds |
FYN_RETRY_MAX_ATTEMPTS | SDK retry max attempts |
FYN_RETRY_INITIAL_DELAY_MS | SDK retry initial delay in milliseconds |
FYN_CACHE_ENABLED | Enable SDK cache |
FYN_CACHE_TTL_MS | SDK cache TTL in milliseconds |
FYN_CACHE_STORAGE | SDK cache storage: memory or disk |
FYN_CACHE_PATH | Disk cache file path |
| Variable | Description |
|---|---|
FYN_DEFAULT_CREATE_DOMAIN | Fallback domain for links.create when omitted |
FYN_DEFAULT_CREATE_SAFE_MODE | Default safeMode for links.create |
FYN_DEFAULT_CREATE_ANALYTICS | Default analytics mode for links.create |
FYN_DEFAULT_CREATE_PRIVATE_LINK | Default privateLink for links.create |
FYN_DEFAULT_CREATE_EXPIRY_SECONDS | Default expirySeconds for links.create |
The current API Hub repository is built as a long-running Node.js service on Hono (@hono/node-server). The default production path is running the published GHCR image with Docker Compose, then placing it behind HTTPS via Nginx or your load balancer.
Use this baseline before exposing API Hub to public or partner traffic.
| Control | Minimum hardening requirement |
|---|---|
Image version | Pin API_HUB_IMAGE_TAG to a tested version tag (vX.Y.Z) instead of latest. |
Inbound auth | Set API_HUB_BEARER_TOKEN and rotate it regularly. |
CORS policy | Set API_HUB_CORS_ORIGIN to trusted origins only when browser clients are involved. |
Secrets handling | Store FYN_API_TOKEN and FYN_SECRET_KEY in a secret manager; never commit them. |
Transport security | Expose API Hub only over HTTPS with TLS terminated at reverse proxy or load balancer. |
Network boundary | Restrict inbound access to approved source ranges and required ports only. |
Observability | Keep /health checks, collect logs, and wire alerting for 4xx/5xx and restart spikes. |
Release process | Promote pinned image tags through staging before production rollout. |
VPS / Bare Metal
How: Run API Hub directly on your own VM or machine.
Setup: Use Node 20+, run via systemd/PM2, terminate TLS with Nginx/Caddy, and keep env vars + logs managed on host.
Railway
How: Deploy as a Node web service.
Setup: Build: npm ci && npm run build. Start: npm run start. Configure env vars in Railway dashboard.
Render
How: Deploy as a Node web service.
Setup: Use the same Node build/start commands and set environment variables in Render service settings.
Fly.io
How: Deploy as a container or Node app.
Setup: Keep one region close to your upstream backend and scale based on traffic patterns.
Cloudflare
How: Use a Workers-target variant.
Setup: Reuse route contracts and switch to a Workers runtime adapter deployed through Wrangler.
Vercel
How: Use a Functions-target variant.
Setup: Expose a Vercel adapter entrypoint and keep environment variables in Vercel project settings.
For Cloudflare/Vercel, keep this API contract the same and maintain a runtime-specific entrypoint. This keeps integration clients stable.
Use these commands when you are operating API Hub in production and want to update images safely.
# update to latest image (brief restart) docker compose pull docker compose up -d
# pin to a specific version API_HUB_IMAGE_TAG=v0.1.1 docker compose pull API_HUB_IMAGE_TAG=v0.1.1 docker compose up -d
# zero downtime (blue/green behind Nginx) NEW_TAG=v0.1.1 docker pull ghcr.io/fynlink/api-hub:${NEW_TAG} docker run -d --name fyn-api-hub-green \ --env-file .env \ -p 8788:8787 \ --restart unless-stopped \ ghcr.io/fynlink/api-hub:${NEW_TAG} curl -fsS http://127.0.0.1:8788/health # switch nginx upstream from :8787 to :8788, then reload sudo nginx -s reload docker stop fyn-api-hub && docker rm fyn-api-hub docker rename fyn-api-hub-green fyn-api-hub
Containerizing API Hub is the most portable production approach when using the current Node runtime implementation. The repository includes a production Dockerfile and a docker-compose.yml that defaults to the official GHCR image.
# compose-based run (recommended) docker compose pull docker compose up -d # local image build (optional) docker build -t fyn-api-hub:latest .
For production, terminate TLS at Nginx and proxy traffic to the Hono Node process on localhost.
server { listen 443 ssl http2; server_name api-hub.example.com; ssl_certificate /etc/letsencrypt/live/api-hub.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/api-hub.example.com/privkey.pem; location / { proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Connection ""; proxy_pass http://127.0.0.1:8787; } }
Pair this with process supervision (`systemd`, Docker restart policy, or managed platform process controls) and health checks on /health .
All tabbed examples on this page are synchronized to one selected language so you can switch once and continue reading without repeated tab changes.
| Language | Recommended HTTP client | Install |
|---|---|---|
Python | requests | pip install requests |
PHP | guzzlehttp/guzzle | composer require guzzlehttp/guzzle |
Go | net/http (stdlib) | No extra package required |
Swift | URLSession (Foundation) | No extra package required |
Kotlin | OkHttp | implementation("com.squareup.okhttp3:okhttp:4.12.0") |
Rust | reqwest blocking client | cargo add reqwest --features blocking,rustls-tls |
API Hub supports optional bearer authentication for inbound callers. If API_HUB_BEARER_TOKEN is set, every request to /v1/* must include an Authorization: Bearer ... header.
curl "http://localhost:8787/v1/team" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN"
Upstream authentication is handled separately through FYN_API_TOKEN and optional FYN_SECRET_KEY.
Base path: /v1
| Method | Path | Description |
|---|---|---|
GET | /health | Health check endpoint |
GET | /v1 | Service and endpoint index |
GET | /v1/info | Get account and domain info |
POST | /v1/links | Create link |
GET | /v1/links | List links |
GET | /v1/links/:id | Get link by ID |
PATCH | /v1/links/:id | Update link fields |
DELETE | /v1/links/:id | Delete link |
POST | /v1/domains | Create domain |
GET | /v1/domains | List domains |
GET | /v1/domains/:id | Get domain |
PATCH | /v1/domains/:id | Update domain |
POST | /v1/domains/:id/verify | Trigger domain verification |
DELETE | /v1/domains/:id | Remove domain |
POST | /v1/members | Invite member |
GET | /v1/members | List members |
GET | /v1/members/:id | Get member |
PATCH | /v1/members/:id | Update member role |
DELETE | /v1/members/:id | Remove member |
GET | /v1/team | Get team profile |
PATCH | /v1/team | Update team name |
GET | /v1/analytics/links/:id/clicks | Link click summary |
GET | /v1/analytics/links/:id/timeseries | Link time series metrics |
GET | /v1/analytics/links/:id/browsers | Browser breakdown |
GET | /v1/analytics/links/:id/devices | Device breakdown |
GET | /v1/analytics/links/:id/countries | Country breakdown |
GET | /v1/analytics/links/:id/referrers | Referrer breakdown |
| Topic | Behavior |
|---|---|
Content type | Use application/json for POST and PATCH request bodies. |
Authentication | Optional bearer auth at API Hub layer via Authorization header. |
Request ID | Include x-request-id to propagate your own tracing ID. |
Boolean query values | true/false and 1/0 are accepted. |
CSV filters | country, browser, device, referrer support comma-separated values. |
Pagination | page and limit must be positive integers. |
Payload strictness | Unknown JSON fields are rejected by schema validation. |
curl "http://localhost:8787/v1/links?page=1&limit=20&safeMode=true" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN" \ -H "x-request-id: req_20260414_001"
Use this endpoint to fetch team-level bootstrap information. Pass force=true to bypass SDK cache for that request.
| Method | Path | Query | Description |
|---|---|---|---|
GET | /v1/info | force (optional boolean) | Returns account info and available domains. |
curl "http://localhost:8787/v1/info?force=true" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN"
| Create field | Type | Required | Notes |
|---|---|---|---|
target | string | Yes | Destination URL. |
domain | string | No | Domain used for the short link. |
slug | string | No | Custom slug. |
title | string | No | Display title. |
notes | string | No | Internal notes. |
tags | string[] | No | Arbitrary tags. |
iosTarget | string | No | iOS deep-link target URL. |
androidTarget | string | No | Android deep-link target URL. |
geoTargets | Record<string,string> | No | Country-code based target map. |
password | string | No | Access password. |
safeMode | boolean | No | Safe mode override. |
privateLink | boolean | No | Private visibility flag. |
analytics | none|full|partial|clicks | No | Tracking level. |
expirySeconds | positive integer | No | Time-to-live in seconds. |
| List query | Type | Description |
|---|---|---|
page | positive integer | Pagination page number. |
limit | positive integer | Items per page. |
sortBy | created_at|updated_at|clicks | Sort field. |
sortOrder | asc|desc | Sort direction. |
domain | string | Filter by domain. |
tracking | none|full|partial|clicks | Filter by analytics mode. |
safeMode | boolean | Filter by safe mode status. |
isPrivate | boolean | Filter by privacy mode. |
password | boolean | Filter password-enabled links. |
curl -X POST "http://localhost:8787/v1/links" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "target": "https://example.com", "domain": "go.example.com", "analytics": "full" }'
curl "http://localhost:8787/v1/links?page=1&limit=20&sortBy=created_at&sortOrder=desc" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN"
curl "http://localhost:8787/v1/links/link_123" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN"
curl -X PATCH "http://localhost:8787/v1/links/link_123" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "title": "Updated title", "safeMode": false }'
curl -X DELETE "http://localhost:8787/v1/links/link_123" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN"
{ "id": "link_123", "domain": "go.example.com", "slug": "launch7", "shortUrl": "https://go.example.com/launch7", "tracking": "full", "safeMode": true, "isPrivate": false }
| Create field | Type | Required | Notes |
|---|---|---|---|
domain | string | Yes | Domain to add to team. |
description | string | No | Optional domain description. |
notFoundUrl | string | No | Destination for unmatched slugs. |
rootRedirect | string | No | Root domain redirect target. |
gpc | boolean | No | Global Privacy Control behavior. |
| Method | Path | Notes |
|---|---|---|
GET | /v1/domains?page=1&limit=20 | List domains |
GET | /v1/domains/:id | Get one domain |
PATCH | /v1/domains/:id | Update domain fields |
POST | /v1/domains/:id/verify | Run verification flow |
DELETE | /v1/domains/:id | Remove domain |
curl -X POST "http://localhost:8787/v1/domains" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "domain": "links.example.com" }'
curl "http://localhost:8787/v1/domains?page=1&limit=20" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN"
curl -X PATCH "http://localhost:8787/v1/domains/domain_123" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "description": "Primary branded domain", "gpc": true }'
curl -X POST "http://localhost:8787/v1/domains/domain_123/verify" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN"
| Field | Type | Required | Notes |
|---|---|---|---|
email | string | Yes | Member email for invitation. |
role | admin|member|readonly | Yes (invite/update) | Role to assign. |
includeOwner | boolean query | No | Include owner in list responses. |
POST /v1/members and DELETE /v1/members/:id return 204 No Content on success.
curl -X POST "http://localhost:8787/v1/members" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "email": "dev@example.com", "role": "member" }'
curl "http://localhost:8787/v1/members?includeOwner=true" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN"
curl -X PATCH "http://localhost:8787/v1/members/member_123" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "role": "admin" }'
curl -X DELETE "http://localhost:8787/v1/members/member_123" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN"
| Method | Path | Body | Description |
|---|---|---|---|
GET | /v1/team | - | Fetch team profile |
PATCH | /v1/team | { "name": "..." } | Update team name |
curl "http://localhost:8787/v1/team" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN"
curl -X PATCH "http://localhost:8787/v1/team" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Growth Team" }'
| Endpoint | Description |
|---|---|
/v1/analytics/links/:id/clicks | Total clicks and aggregate link metrics. |
/v1/analytics/links/:id/timeseries | Time-bucketed analytics series. |
/v1/analytics/links/:id/browsers | Browser distribution. |
/v1/analytics/links/:id/devices | Device distribution. |
/v1/analytics/links/:id/countries | Country distribution. |
/v1/analytics/links/:id/referrers | Referrer distribution. |
| Query | Type | Notes |
|---|---|---|
start | string | Range start (ISO datetime recommended). |
end | string | Range end (ISO datetime recommended). |
granularity | minute|hour|day|week|month | Timeseries interval. |
country | CSV string | Example: US,IN,DE |
browser | CSV string | Example: Chrome,Firefox |
device | CSV string | Example: Desktop,Mobile |
referrer | CSV string | Example: google.com,news.ycombinator.com |
curl "http://localhost:8787/v1/analytics/links/link_123/clicks" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN"
curl "http://localhost:8787/v1/analytics/links/link_123/timeseries?start=2026-04-01T00:00:00Z&end=2026-04-14T23:59:59Z&granularity=day" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN"
curl "http://localhost:8787/v1/analytics/links/link_123/countries?start=2026-04-01T00:00:00Z&end=2026-04-14T23:59:59Z&country=US,IN,DE" \ -H "Authorization: Bearer $API_HUB_BEARER_TOKEN"
API Hub returns normalized non-2xx responses with an explicit error code and request ID for tracing.
{ "error": { "code": "UPSTREAM_VALIDATION_ERROR", "message": "domain is required", "details": {} }, "requestId": "req_20260414_001" }
| HTTP | Code | When it happens |
|---|---|---|
400 | VALIDATION_ERROR / HTTP_ERROR / UPSTREAM_VALIDATION_ERROR | Invalid payload, malformed query, or upstream validation failure. |
401 | UPSTREAM_AUTH_ERROR / HTTP_ERROR | Invalid upstream token or missing/invalid API_HUB_BEARER_TOKEN. |
403 | UPSTREAM_PERMISSION_ERROR | Token lacks permissions for requested team resource. |
404 | NOT_FOUND / UPSTREAM_NOT_FOUND | Unknown route or resource ID not found upstream. |
429 | UPSTREAM_RATE_LIMITED | Upstream API rate limit reached. |
500 | INTERNAL_ERROR | Unexpected API Hub runtime error. |
502 | UPSTREAM_ERROR | Generic upstream failure with unknown mapping. |
504 | UPSTREAM_TIMEOUT | Upstream timeout while SDK request is in progress. |
API Hub is a proxy. Upstream validations and business rules remain on Fynlink backend. The hub focuses on secure transport, consistent REST interfaces, and operational ergonomics for multi-language client stacks.