337 lines
7.4 KiB
Markdown
337 lines
7.4 KiB
Markdown
# VC Gateway — API Contract
|
|
|
|
**Module:** `/api/hub/vcgateway/`
|
|
**Owner:** @mike (backend), @netasha (frontend)
|
|
**Status:** Implemented on dev (2026-03-27)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
The VC Gateway enables anonymous visitors (e.g. investors) to:
|
|
1. Authenticate via a shareable invite link (no account needed)
|
|
2. Read messages from scoped channels (read-only)
|
|
3. Send DMs to the host (rate-limited)
|
|
|
|
Two auth models:
|
|
- **Agent auth** (`X-Agent-Address` header) — for team members managing invite links
|
|
- **Visitor auth** (`X-Visitor-Token` header) — for anonymous visitors using the gateway
|
|
|
|
---
|
|
|
|
## Endpoints
|
|
|
|
### Invite Management (Agent Auth)
|
|
|
|
#### `POST /api/hub/vcgateway/invites/create.php`
|
|
|
|
Create an invite link.
|
|
|
|
**Headers:** `X-Agent-Address: sprinter.payfrit.john`
|
|
|
|
**Body:**
|
|
```json
|
|
{
|
|
"AllowedChannels": [1, 5, 12],
|
|
"HostAddress": "sprinter.payfrit.john",
|
|
"Label": "Sequoia - Partner X",
|
|
"ExpiresAt": "2026-04-15T00:00:00Z",
|
|
"MaxUses": 3
|
|
}
|
|
```
|
|
|
|
| Field | Type | Required | Notes |
|
|
|-------|------|----------|-------|
|
|
| AllowedChannels | int[] | ✅ | Hub channel IDs visitor can read |
|
|
| HostAddress | string | ✅ | Sprinter agent address for DM target |
|
|
| Label | string | ❌ | Human label (max 255 chars) |
|
|
| ExpiresAt | string | ❌ | ISO8601 datetime; null = never |
|
|
| MaxUses | int | ❌ | 0 = unlimited (default) |
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"OK": true,
|
|
"ID": 1,
|
|
"Token": "a1b2c3d4e5f6...",
|
|
"InviteURL": "https://dev.payfrit.com/vc/a1b2c3d4e5f6..."
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
#### `POST /api/hub/vcgateway/invites/revoke.php`
|
|
|
|
Revoke an invite link. All visitor sessions become invalid.
|
|
|
|
**Headers:** `X-Agent-Address: sprinter.payfrit.john`
|
|
|
|
**Body:**
|
|
```json
|
|
{ "ID": 1 }
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{ "OK": true }
|
|
```
|
|
|
|
---
|
|
|
|
#### `GET /api/hub/vcgateway/invites/list.php`
|
|
|
|
List invite links with optional status filter.
|
|
|
|
**Headers:** `X-Agent-Address: sprinter.payfrit.john`
|
|
|
|
**Query params:**
|
|
|
|
| Param | Type | Default | Options |
|
|
|-------|------|---------|---------|
|
|
| Status | string | all | `active`, `revoked`, `expired`, `all` |
|
|
| Limit | int | 50 | max 200 |
|
|
| Offset | int | 0 | |
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"OK": true,
|
|
"Links": [
|
|
{
|
|
"ID": 1,
|
|
"Token": "a1b2c3...",
|
|
"Label": "Sequoia - Partner X",
|
|
"AllowedChannels": [1, 5, 12],
|
|
"HostAddress": "sprinter.payfrit.john",
|
|
"ExpiresAt": "2026-04-15T00:00:00Z",
|
|
"MaxUses": 3,
|
|
"UseCount": 1,
|
|
"VisitorCount": 1,
|
|
"Status": "active",
|
|
"CreatedBy": "sprinter.payfrit.john",
|
|
"CreatedAt": "2026-03-27T20:00:00Z"
|
|
}
|
|
],
|
|
"Total": 1
|
|
}
|
|
```
|
|
|
|
**Status values:** `active`, `revoked`, `expired`, `exhausted`
|
|
|
|
---
|
|
|
|
#### `GET /api/hub/vcgateway/invites/get.php`
|
|
|
|
Get a single invite link with its visitors.
|
|
|
|
**Headers:** `X-Agent-Address: sprinter.payfrit.john`
|
|
|
|
**Query params:** `ID=1` or `Token=a1b2c3...`
|
|
|
|
**Response:** Same as list item, plus a `Visitors` array:
|
|
```json
|
|
{
|
|
"OK": true,
|
|
"Link": {
|
|
"...": "same fields as list",
|
|
"Visitors": [
|
|
{
|
|
"ID": 1,
|
|
"DisplayName": "Alex Chen",
|
|
"CreatedAt": "2026-03-27T21:00:00Z",
|
|
"LastActiveAt": "2026-03-27T21:15:00Z"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Visitor Endpoints (Visitor Auth)
|
|
|
|
#### `POST /api/hub/vcgateway/visitor/auth.php`
|
|
|
|
Authenticate as a visitor. No auth header needed — uses the invite token.
|
|
|
|
**Body:**
|
|
```json
|
|
{
|
|
"InviteToken": "a1b2c3d4e5f6...",
|
|
"DisplayName": "Alex Chen"
|
|
}
|
|
```
|
|
|
|
| Field | Type | Required | Notes |
|
|
|-------|------|----------|-------|
|
|
| InviteToken | string | ✅ | Token from invite URL |
|
|
| DisplayName | string | ❌ | Default: "Visitor" (max 100 chars) |
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"OK": true,
|
|
"VisitorToken": "x9y8z7...",
|
|
"VisitorID": 1,
|
|
"DisplayName": "Alex Chen",
|
|
"AllowedChannels": [
|
|
{
|
|
"ID": 1,
|
|
"Name": "general",
|
|
"DisplayName": "General",
|
|
"Purpose": "Team discussion",
|
|
"ChannelType": "public"
|
|
}
|
|
],
|
|
"HostAddress": "sprinter.payfrit.john"
|
|
}
|
|
```
|
|
|
|
**Save `VisitorToken` — use it as `X-Visitor-Token` header for all subsequent calls.**
|
|
|
|
**Error codes:** `invalid_invite_token`, `invite_revoked`, `invite_expired`, `invite_exhausted`
|
|
|
|
---
|
|
|
|
#### `GET /api/hub/vcgateway/visitor/feed.php`
|
|
|
|
Read-only message feed for an allowed channel.
|
|
|
|
**Headers:** `X-Visitor-Token: x9y8z7...`
|
|
|
|
**Query params:**
|
|
|
|
| Param | Type | Required | Notes |
|
|
|-------|------|----------|-------|
|
|
| ChannelID | int | ✅ | Must be in visitor's AllowedChannels |
|
|
| Before | int | ❌ | Cursor: messages before this ID |
|
|
| After | int | ❌ | Cursor: messages after this ID |
|
|
| Limit | int | ❌ | Default 50, max 100 |
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"OK": true,
|
|
"Channel": {
|
|
"ID": 1,
|
|
"Name": "general",
|
|
"DisplayName": "General",
|
|
"Purpose": "Team discussion"
|
|
},
|
|
"Messages": [
|
|
{
|
|
"ID": 100,
|
|
"SenderAddress": "sprinter.payfrit.john",
|
|
"SenderName": "john",
|
|
"Content": "Let's discuss the roadmap",
|
|
"ParentID": null,
|
|
"IsEdited": false,
|
|
"CreatedAt": "2026-03-27T20:30:00Z"
|
|
}
|
|
],
|
|
"HasMore": true
|
|
}
|
|
```
|
|
|
|
**Error codes:** `channel_id_required`, `channel_not_allowed`, `channel_not_found`
|
|
|
|
---
|
|
|
|
### DM Endpoints (Visitor Auth)
|
|
|
|
#### `POST /api/hub/vcgateway/dm/send.php`
|
|
|
|
Send a DM from visitor to host. Rate-limited (10 messages per 5 minutes).
|
|
|
|
**Headers:** `X-Visitor-Token: x9y8z7...`
|
|
|
|
**Body:**
|
|
```json
|
|
{ "Content": "Hi, I had a question about your Series A terms." }
|
|
```
|
|
|
|
| Field | Type | Required | Notes |
|
|
|-------|------|----------|-------|
|
|
| Content | string | ✅ | Max 4000 chars |
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"OK": true,
|
|
"MessageID": 201,
|
|
"DMChannelID": 15,
|
|
"CreatedAt": "2026-03-27T21:00:00Z"
|
|
}
|
|
```
|
|
|
|
**Rate limit error (HTTP 429):**
|
|
```json
|
|
{
|
|
"OK": false,
|
|
"ERROR": "rate_limit_exceeded",
|
|
"Limit": 10,
|
|
"WindowSeconds": 300,
|
|
"ResetsAt": "2026-03-27T21:05:00Z"
|
|
}
|
|
```
|
|
|
|
The DM channel is auto-created on first message. The host sees DMs in their normal Hub message list (channel type: `direct`).
|
|
|
|
---
|
|
|
|
#### `GET /api/hub/vcgateway/dm/messages.php`
|
|
|
|
List messages in the visitor's DM channel.
|
|
|
|
**Headers:** `X-Visitor-Token: x9y8z7...`
|
|
|
|
**Query params:**
|
|
|
|
| Param | Type | Required | Notes |
|
|
|-------|------|----------|-------|
|
|
| Before | int | ❌ | Cursor: messages before this ID |
|
|
| After | int | ❌ | Cursor: messages after this ID |
|
|
| Limit | int | ❌ | Default 50, max 100 |
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"OK": true,
|
|
"Messages": [
|
|
{
|
|
"ID": 201,
|
|
"SenderAddress": "visitor:1:Alex Chen",
|
|
"SenderName": "Alex Chen",
|
|
"IsVisitor": true,
|
|
"Content": "Hi, I had a question about your Series A terms.",
|
|
"IsEdited": false,
|
|
"CreatedAt": "2026-03-27T21:00:00Z"
|
|
},
|
|
{
|
|
"ID": 205,
|
|
"SenderAddress": "sprinter.payfrit.john",
|
|
"SenderName": "john",
|
|
"IsVisitor": false,
|
|
"Content": "Sure, happy to discuss. What are you thinking?",
|
|
"IsEdited": false,
|
|
"CreatedAt": "2026-03-27T21:02:00Z"
|
|
}
|
|
],
|
|
"DMChannelID": 15,
|
|
"HasMore": false
|
|
}
|
|
```
|
|
|
|
Returns empty `Messages` array if no DM channel exists yet.
|
|
|
|
---
|
|
|
|
## Architecture Notes
|
|
|
|
- **Tables:** `Hub_InviteLinks`, `Hub_Visitors`, `Hub_VisitorRateLimit`
|
|
- **DM channels** use existing `Hub_Channels` (type: `direct`) and `Hub_Messages`
|
|
- **Rate limiting:** 10 messages per 5-minute sliding window, stored in `Hub_VisitorRateLimit`
|
|
- **Visitor sender format:** `visitor:{id}:{displayName}` in `SenderAddress`
|
|
- **Host replies** use normal Hub message send (existing endpoint), targeting the DM channel
|
|
- **Revoking a link** invalidates all visitor sessions derived from it
|
|
- **Schema:** `/api/hub/vcgateway/schema.sql`
|