{
 "openapi": "3.1.0",
 "info": {
  "title": "server-track-lab API",
  "version": "0.1.0",
  "description": "Server-side conversion API for B2B SaaS by Marc Stevens. One call normalizes, hashes, dedupes on event_id and forwards to Meta Conversions API. Google Enhanced Conversions: coming soon (preview only). LinkedIn: roadmap. Free 1,000 events/month, then CA$2 per 1,000.",
  "contact": {
   "name": "Marc Stevens",
   "url": "https://server-track-lab.pages.dev/"
  }
 },
 "servers": [
  {
   "url": "https://server-track-lab.pages.dev"
  }
 ],
 "externalDocs": {
  "url": "https://server-track-lab.pages.dev/docs"
 },
 "paths": {
  "/api/v1/track": {
   "post": {
    "operationId": "track",
    "summary": "Send one conversion event",
    "security": [
     {
      "bearer": []
     },
     {
      "apiKey": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/TrackEvent"
       },
       "example": {
        "event_name": "demo_booked",
        "event_id": "demo_booked:8f3a2c71",
        "timestamp": "2026-10-14T15:04:05Z",
        "user": {
         "email": "jane@example.com"
        },
        "action_source": "system_generated"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "accepted or deduplicated",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/TrackResponse"
        }
       }
      }
     },
     "400": {
      "description": "invalid_json",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "401": {
      "description": "unauthorized",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "402": {
      "description": "quota_exceeded",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "405": {
      "description": "method_not_allowed",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "413": {
      "description": "payload_too_large (32 KB)",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "unsupported_media_type",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "422": {
      "description": "validation_error",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/v1/health": {
   "get": {
    "operationId": "health",
    "summary": "Service health",
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "ok": {
           "type": "boolean"
          },
          "service": {
           "type": "string"
          },
          "version": {
           "type": "string"
          },
          "google_adapter": {
           "type": "string"
          }
         },
         "required": [
          "ok",
          "service"
         ]
        }
       }
      }
     },
     "405": {
      "description": "method_not_allowed",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "security": []
   }
  },
  "/api/v1/playground": {
   "post": {
    "operationId": "playground",
    "summary": "Dry-run an event without an API key",
    "description": "Public, Turnstile-protected, 20 runs/hour per IP. Stores nothing and counts no quota. Returns validation, the exact Meta CAPI body /track would send, a Google preview and dedup notes.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "event",
         "turnstile_token"
        ],
        "properties": {
         "event": {
          "$ref": "#/components/schemas/TrackEvent"
         },
         "turnstile_token": {
          "type": "string"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "valid event preview",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/PlaygroundResponse"
        }
       }
      }
     },
     "422": {
      "description": "invalid event (same shape, status=invalid) or missing event",
      "content": {
       "application/json": {
        "schema": {
         "oneOf": [
          {
           "$ref": "#/components/schemas/PlaygroundResponse"
          },
          {
           "$ref": "#/components/schemas/Error"
          }
         ]
        }
       }
      }
     },
     "403": {
      "description": "turnstile_failed",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "rate_limited",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "playground_disabled",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "security": []
   }
  }
 },
 "components": {
  "securitySchemes": {
   "bearer": {
    "type": "http",
    "scheme": "bearer",
    "description": "sst_live_\u2026 or sst_test_\u2026 key from /signup"
   },
   "apiKey": {
    "type": "apiKey",
    "in": "header",
    "name": "X-API-Key"
   }
  },
  "schemas": {
   "FieldIssue": {
    "type": "object",
    "properties": {
     "field": {
      "type": "string"
     },
     "issue": {
      "type": "string"
     }
    },
    "required": [
     "field",
     "issue"
    ]
   },
   "Error": {
    "type": "object",
    "properties": {
     "error": {
      "type": "object",
      "properties": {
       "code": {
        "type": "string"
       },
       "message": {
        "type": "string"
       },
       "fields": {
        "type": "array",
        "items": {
         "$ref": "#/components/schemas/FieldIssue"
        }
       },
       "request_id": {
        "type": "string"
       }
      },
      "required": [
       "code",
       "message"
      ]
     }
    },
    "required": [
     "error"
    ]
   },
   "User": {
    "type": "object",
    "description": "At least one identifier is required: email, phone, external_id, fbp, fbc, fbclid, gclid, gbraid or wbraid. Raw values are normalized and SHA-256 hashed server-side; 64-char lowercase hex values are treated as pre-hashed.",
    "properties": {
     "email": {
      "type": "string",
      "maxLength": 2048
     },
     "phone": {
      "type": "string",
      "maxLength": 2048,
      "description": "7-15 digits; include a country code (or set a workspace default)."
     },
     "external_id": {
      "type": "string",
      "maxLength": 256
     },
     "first_name": {
      "type": "string",
      "maxLength": 2048
     },
     "last_name": {
      "type": "string",
      "maxLength": 2048
     },
     "country": {
      "type": "string",
      "maxLength": 2048,
      "description": "ISO 3166-1 alpha-2, e.g. CA"
     },
     "ip": {
      "type": "string",
      "maxLength": 2048
     },
     "user_agent": {
      "type": "string",
      "maxLength": 2048
     },
     "fbp": {
      "type": "string",
      "maxLength": 2048,
      "pattern": "^fb\\.\\d+\\.\\d+\\..+$"
     },
     "fbc": {
      "type": "string",
      "maxLength": 2048,
      "pattern": "^fb\\.\\d+\\.\\d+\\..+$"
     },
     "fbclid": {
      "type": "string",
      "maxLength": 2048
     },
     "gclid": {
      "type": "string",
      "maxLength": 2048
     },
     "gbraid": {
      "type": "string",
      "maxLength": 2048
     },
     "wbraid": {
      "type": "string",
      "maxLength": 2048
     }
    }
   },
   "TrackEvent": {
    "type": "object",
    "additionalProperties": false,
    "required": [
     "event_name",
     "event_id",
     "timestamp",
     "user"
    ],
    "properties": {
     "event_name": {
      "type": "string",
      "pattern": "^[A-Za-z0-9_]{1,64}$",
      "description": "Lowercased. Mapped to Meta: signup\u2192CompleteRegistration, lead\u2192Lead, trial_started\u2192StartTrial, demo_booked\u2192Schedule, contact\u2192Contact, qualified\u2192Lead (event_stage=qualified), opportunity\u2192qualified_opportunity, subscribed\u2192Subscribe, closed_won\u2192Purchase (requires value+currency). Other names pass through."
     },
     "event": {
      "type": "string",
      "description": "Alias of event_name; if both are sent they must be equal."
     },
     "event_id": {
      "type": "string",
      "maxLength": 128,
      "pattern": "^[A-Za-z0-9._:\\-]+$",
      "description": "Dedup key. Repeats within 72h return status=deduplicated; forwarded unchanged to Meta."
     },
     "timestamp": {
      "type": "string",
      "format": "date-time",
      "description": "ISO 8601 with Z or offset; not more than 5 minutes in the future. Meta drops events older than 7 days."
     },
     "user": {
      "$ref": "#/components/schemas/User"
     },
     "value": {
      "type": "number",
      "minimum": 0,
      "maximum": 1000000000,
      "description": "Max 2 decimals. currency required when value > 0."
     },
     "currency": {
      "type": "string",
      "pattern": "^[A-Z]{3}$"
     },
     "event_source_url": {
      "type": "string",
      "format": "uri",
      "maxLength": 2048
     },
     "action_source": {
      "type": "string",
      "enum": [
       "website",
       "system_generated",
       "crm",
       "phone_call",
       "email",
       "chat",
       "other"
      ],
      "description": "crm is sent to Meta as system_generated. website without user.user_agent is downgraded to system_generated."
     },
     "consent": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
       "ad_user_data": {
        "type": "string",
        "enum": [
         "granted",
         "denied"
        ]
       },
       "ad_personalization": {
        "type": "string",
        "enum": [
         "granted",
         "denied"
        ]
       }
      }
     },
     "destinations": {
      "type": "array",
      "minItems": 1,
      "items": {
       "type": "string",
       "enum": [
        "meta",
        "google"
       ]
      },
      "description": "Google is a preview stub (not delivered yet)."
     },
     "test": {
      "type": "boolean",
      "default": false,
      "description": "Send to Meta Test Events using the configured test_event_code."
     },
     "properties": {
      "type": "object",
      "maxProperties": 20,
      "additionalProperties": {
       "type": [
        "string",
        "number",
        "boolean",
        "null"
       ]
      },
      "description": "Scalar values only, <= 2 KB JSON. Merged into Meta custom_data."
     }
    }
   },
   "TrackResponse": {
    "type": "object",
    "required": [
     "status",
     "event_id"
    ],
    "properties": {
     "status": {
      "type": "string",
      "enum": [
       "accepted",
       "deduplicated"
      ]
     },
     "retry": {
      "type": "boolean"
     },
     "event_id": {
      "type": "string"
     },
     "received_at": {
      "type": "string",
      "format": "date-time"
     },
     "first_received_at": {
      "type": "string",
      "format": "date-time"
     },
     "destinations": {
      "type": "object",
      "additionalProperties": {
       "type": "object",
       "properties": {
        "status": {
         "type": "string",
         "enum": [
          "sent",
          "skipped",
          "failed",
          "pending",
          "preview_only"
         ]
        },
        "reason": {
         "type": "string"
        }
       },
       "additionalProperties": true
      }
     },
     "warnings": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "usage": {
      "type": "object",
      "additionalProperties": true
     }
    }
   },
   "PlaygroundResponse": {
    "type": "object",
    "properties": {
     "status": {
      "type": "string",
      "enum": [
       "ok",
       "invalid"
      ]
     },
     "stored": {
      "type": "boolean",
      "const": false
     },
     "quota_counted": {
      "type": "boolean",
      "const": false
     },
     "meta_delivery": {
      "type": "object",
      "properties": {
       "mode": {
        "type": "string",
        "enum": [
         "none",
         "dry_run",
         "skipped",
         "test_event_forwarded"
        ]
       },
       "message": {
        "type": "string"
       }
      },
      "additionalProperties": true
     },
     "validation": {
      "type": "object",
      "additionalProperties": true
     },
     "meta": {
      "type": "object",
      "additionalProperties": true
     },
     "google": {
      "type": "object",
      "additionalProperties": true
     },
     "dedup": {}
    },
    "additionalProperties": true
   }
  }
 }
}