payfrit-api/api/hub/vcgateway/API-CONTRACT.md
Mike cd373dd616 Add VC Gateway endpoints for invite links, visitor auth, DM, and rate limiting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:34:52 +00:00

7.4 KiB

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:

{
  "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:

{
  "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:

{ "ID": 1 }

Response:

{ "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:

{
  "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:

{
  "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:

{
  "InviteToken": "a1b2c3d4e5f6...",
  "DisplayName": "Alex Chen"
}
Field Type Required Notes
InviteToken string Token from invite URL
DisplayName string Default: "Visitor" (max 100 chars)

Response:

{
  "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:

{
  "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:

{ "Content": "Hi, I had a question about your Series A terms." }
Field Type Required Notes
Content string Max 4000 chars

Response:

{
  "OK": true,
  "MessageID": 201,
  "DMChannelID": 15,
  "CreatedAt": "2026-03-27T21:00:00Z"
}

Rate limit error (HTTP 429):

{
  "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:

{
  "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