Skip to main content

Submit W9

POST /w9/submit
endpoint
Submit a W9 form for a wallet address.
Authentication: None (public endpoint) Request Body:
{
  "wallet_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1",
  "email": "[email protected]",
  "year": 2026
}
Response: 201 Created
{
  "submission": {
    "id": 1,
    "wallet_address": "0x742d35cc6634c0532925a3b844bc9e7595f0beb1",
    "year": 2026,
    "email": "[email protected]",
    "submitted_at": "2026-03-04T10:00:00Z",
    "pending_approval": true,
    "approved_at": null,
    "approved_by_user_id": null,
    "rejected_at": null,
    "rejected_by_user_id": null,
    "rejection_reason": null
  }
}
Error Responses:
  • 409 Conflict - W9 already submitted for this wallet/year (returns {"error": "w9_pending"} or {"error": "w9_approved"})

W9 Webhook

POST /w9/webhook
endpoint
Webhook endpoint for external W9 submission systems (e.g., WordPress).
Authentication: Header X-W9-Secret or X-W9-Key must match W9_WEBHOOK_SECRET environment variable Content Types Supported:
  • application/json
  • application/x-www-form-urlencoded
Request Body (JSON):
{
  "wallet_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1",
  "email": "[email protected]",
  "year": 2026
}
Request Body (Form Data):
wallet=0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1
&[email protected]
&year=2026
Alternate form field names:
  • wallet_address or wallet
  • year (optional, defaults to current year)
Response: 201 Created Same response format as POST /w9/submit. Error Responses:
  • 403 Forbidden - Invalid or missing webhook secret
  • 409 Conflict - W9 already submitted

Check W9 Compliance

POST /w9/check
endpoint
Check if a transaction is W9 compliant (admin only).
Authentication: Required (Admin role or X-Admin-Key header) Request Body:
{
  "from_address": "0x1234567890123456789012345678901234567890",
  "to_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1",
  "amount": "100000000000000000000"
}
Field Descriptions:
  • from_address - Must be in the PAID_ADMIN_ADDRESSES list to trigger W9 checks
  • to_address - Recipient wallet address
  • amount - Amount in wei (string)
Response: 200 OK (allowed) or 403 Forbidden (blocked)
{
  "allowed": true,
  "email": "[email protected]",
  "current_total": "450000000000000000000",
  "new_total": "550000000000000000000",
  "limit": "600000000000000000000",
  "year": 2026
}
When Blocked (403):
{
  "allowed": false,
  "reason": "w9_required",
  "w9_url": "https://example.com/w9-form",
  "email": "[email protected]",
  "current_total": "650000000000000000000",
  "new_total": "750000000000000000000",
  "limit": "600000000000000000000",
  "year": 2026
}
Possible reason values:
  • w9_required - User needs to submit W9 (over $600 threshold)
  • w9_pending - W9 submitted but not yet approved

Record W9 Transaction

POST /w9/transaction
endpoint
Record a completed payment transaction for W9 tracking (admin only).
Authentication: Required (Admin role or X-Admin-Key header) Request Body:
{
  "from_address": "0x1234567890123456789012345678901234567890",
  "to_address": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb1",
  "amount": "100000000000000000000",
  "hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
  "timestamp": 1709553600
}
Response: 200 OK

Admin: Get Pending W9 Submissions

GET /admin/w9/pending
endpoint
Retrieve all pending W9 submissions (admin only).
Authentication: Required (Admin role or X-Admin-Key header) Response: 200 OK
{
  "submissions": [
    {
      "id": 1,
      "wallet_address": "0x742d35cc6634c0532925a3b844bc9e7595f0beb1",
      "year": 2026,
      "email": "[email protected]",
      "submitted_at": "2026-03-04T10:00:00Z",
      "pending_approval": true,
      "approved_at": null,
      "user_contact_email": "[email protected]"
    }
  ]
}

Admin: Approve W9 Submission

PUT /admin/w9/approve
endpoint
Approve a pending W9 submission (admin only).
Authentication: Required (Admin role or X-Admin-Key header) Request Body:
{
  "id": 1
}
Response: 200 OK
{
  "submission": {
    "id": 1,
    "wallet_address": "0x742d35cc6634c0532925a3b844bc9e7595f0beb1",
    "year": 2026,
    "email": "[email protected]",
    "submitted_at": "2026-03-04T10:00:00Z",
    "pending_approval": false,
    "approved_at": "2026-03-04T11:00:00Z",
    "approved_by_user_id": "did:key:z6Mk..."
  }
}
An approval email is sent to the user.

Admin: Reject W9 Submission

PUT /admin/w9/reject
endpoint
Reject a pending W9 submission (admin only).
Authentication: Required (Admin role or X-Admin-Key header) Request Body:
{
  "id": 1,
  "reason": "Invalid taxpayer information"
}
Response: 200 OK
{
  "submission": {
    "id": 1,
    "wallet_address": "0x742d35cc6634c0532925a3b844bc9e7595f0beb1",
    "year": 2026,
    "email": "[email protected]",
    "submitted_at": "2026-03-04T10:00:00Z",
    "pending_approval": false,
    "rejected_at": "2026-03-04T11:00:00Z",
    "rejected_by_user_id": "did:key:z6Mk...",
    "rejection_reason": "Invalid taxpayer information"
  }
}

Schemas

W9Submission

interface W9Submission {
  id: number;
  wallet_address: string;     // Normalized lowercase address
  year: number;               // Tax year
  email: string;              // Contact email
  submitted_at: string;       // ISO 8601 timestamp
  pending_approval: boolean;  // Approval status
  approved_at: string | null; // ISO 8601 timestamp
  approved_by_user_id: string | null; // Admin DID
  rejected_at: string | null; // ISO 8601 timestamp
  rejected_by_user_id: string | null; // Admin DID
  rejection_reason: string | null;
  user_contact_email?: string | null; // From user profile
}

W9CheckResponse

interface W9CheckResponse {
  allowed: boolean;           // Transaction allowed
  reason?: string;            // "w9_required" | "w9_pending"
  w9_url?: string;            // W9 submission form URL
  email?: string;             // User email
  current_total?: string;     // Current year earnings (wei)
  new_total?: string;         // Earnings after transaction (wei)
  limit?: string;             // W9 threshold (wei, typically $600)
  year?: number;              // Tax year
}

W9WalletEarning

interface W9WalletEarning {
  wallet_address: string;
  year: number;
  amount_received: string;    // Total received (wei)
  user_id?: string | null;    // Associated user DID
  w9_required: boolean;       // Over threshold
  w9_required_at?: string | null; // ISO 8601 timestamp
  last_tx_hash?: string | null;
  last_tx_timestamp?: number | null; // Unix timestamp
}

Build docs developers (and LLMs) love