{"openapi":"3.1.0","info":{"title":"Normalyze","description":"A machine-to-machine API that normalises raw financial transaction strings into strictly typed, categorised JSON. Designed for AI agents and fintech applications consuming Indian banking data.\n\n**v2.1** — Supabase backend, self-serve key generation with Stripe customer auto-creation, Gemini LLM fallback, CSV uploads, and usage auditing.","version":"2.1.0"},"paths":{"/v1/normalize/transaction":{"post":{"tags":["Normalization"],"summary":"Normalize a single transaction","description":"Accepts a single raw financial transaction string and returns a strictly typed JSON object with the cleaned merchant name, spending category, payment channel, normalized timestamp, transaction direction, confidence score, and fallback flag.","operationId":"normalize_single_v1_normalize_transaction_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SingleTransactionRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SingleTransactionResponse"}}}},"400":{"description":"Malformed request body."},"401":{"description":"Missing or invalid API key."},"422":{"description":"Validation error on the input payload."},"500":{"description":"Internal processing error."}},"security":[{"BearerAuth":[]}]}},"/v1/normalize/batch":{"post":{"tags":["Normalization"],"summary":"Normalize a batch of transactions","description":"Accepts up to 500 raw transaction strings and returns an array of normalized results. Usage is logged and a Stripe metered billing event is fired in the background.","operationId":"normalize_batch_v1_normalize_batch_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchTransactionRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchTransactionResponse"}}}},"400":{"description":"Malformed request body."},"401":{"description":"Missing or invalid API key."},"422":{"description":"Validation error on the input payload."},"500":{"description":"Internal processing error."}},"security":[{"BearerAuth":[]}]}},"/v1/normalize/csv":{"post":{"tags":["Normalization"],"summary":"Upload a CSV bank statement","description":"Accepts a CSV file upload (bank statement dump), auto-detects the transaction description column, and normalizes all rows. Maximum file size: 5 MB, maximum rows: 5000.","operationId":"normalize_csv_v1_normalize_csv_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_normalize_csv_v1_normalize_csv_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CSVUploadResponse"}}}},"400":{"description":"CSV parsing error or no valid column found."},"401":{"description":"Missing or invalid API key."},"413":{"description":"File too large (max 5 MB)."},"500":{"description":"Internal processing error."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"BearerAuth":[]}]}},"/v1/keys/generate":{"post":{"tags":["Key Management"],"summary":"Step 1 — Request an API key (sends OTP)","description":"Validates the email, checks for existing keys, then sends a 6-digit verification code to the email via Resend. The OTP expires in 10 minutes. No key is created at this step — call /v1/keys/verify next.","operationId":"generate_key_v1_keys_generate_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateKeyRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OTPSentResponse"}}}},"400":{"description":"Invalid email format."},"409":{"description":"Email already has an active API key."},"500":{"description":"Email delivery or database error."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/keys/verify":{"post":{"tags":["Key Management"],"summary":"Step 2 — Verify OTP and get your API key","description":"Submit the 6-digit OTP from your email along with the email address. If valid and not expired, the API key is created immediately and returned. The raw key is shown only once.","operationId":"verify_otp_v1_keys_verify_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyOTPRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateKeyResponse"}}}},"400":{"description":"Invalid or expired OTP."},"409":{"description":"Email already has an active API key."},"500":{"description":"Stripe or database error."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/keys":{"get":{"tags":["Key Management"],"summary":"List all API keys (admin)","description":"Returns metadata for all API keys. Never exposes raw keys or hashes.","operationId":"list_keys_v1_keys_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListKeysResponse"}}}},"403":{"description":"Admin access required."}},"security":[{"BearerAuth":[]}]}},"/v1/keys/revoke":{"post":{"tags":["Key Management"],"summary":"Revoke an API key (admin)","description":"Soft-deletes an API key by prefix. The key remains in the database but is marked inactive.","operationId":"revoke_key_v1_keys_revoke_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokeKeyRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"object","title":"Response Revoke Key V1 Keys Revoke Post"}}}},"403":{"description":"Admin access required."},"404":{"description":"No active key found with that prefix."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"BearerAuth":[]}]}},"/v1/contact":{"post":{"tags":["Contact"],"summary":"Submit a contact message","description":"Saves the message to Supabase and sends an email notification to the site owner via Brevo. No auth required.","operationId":"submit_contact_v1_contact_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/demo":{"post":{"tags":["Demo"],"summary":"Public demo — normalize a single transaction","description":"A free, unauthenticated endpoint for the landing page demo. Normalizes a single raw transaction string using the same pipeline as /v1/normalize/transaction. Rate-limited to 30 requests per minute per IP.","operationId":"demo_normalize_v1_demo_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DemoRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SingleTransactionResponse"}}}},"422":{"description":"Validation error on the input payload."},"429":{"description":"Rate limit exceeded (30 req/min per IP)."},"500":{"description":"Internal processing error."}}}},"/health":{"get":{"tags":["System"],"summary":"Health check","description":"Returns service status, name, and version. No auth required.","operationId":"health_check_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/":{"get":{"summary":"Landing","operationId":"landing__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/terms":{"get":{"summary":"Terms","operationId":"terms_terms_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/errors":{"get":{"summary":"Errors","operationId":"errors_errors_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/documentation":{"get":{"summary":"Documentation","operationId":"documentation_documentation_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"BatchTransactionRequest":{"properties":{"transactions":{"items":{"type":"string"},"type":"array","maxItems":500,"minItems":1,"title":"Transactions","description":"A list of raw transaction strings (max 500 per batch).","examples":[["UPI/ZOMATO/123456789/PAYMENT","NEFT/SALARY/CR"]]}},"type":"object","required":["transactions"],"title":"BatchTransactionRequest","description":"Accepts a list of raw financial transaction strings for bulk normalization.\n\nThe batch is capped at 500 items per request to prevent abuse and to keep\nresponse latency predictable under serverless execution limits.\n\nExample payload:\n    {\"transactions\": [\"UPI/ZOMATO/123456789/PAYMENT\", \"NEFT/SALARY/CR\"]}"},"BatchTransactionResponse":{"properties":{"status":{"type":"string","title":"Status","default":"success"},"count":{"type":"integer","title":"Count","description":"Number of transactions processed."},"data":{"items":{"$ref":"#/components/schemas/NormalizedTransaction"},"type":"array","title":"Data"}},"type":"object","required":["count","data"],"title":"BatchTransactionResponse","description":"Wraps a batch of normalized transaction results."},"Body_normalize_csv_v1_normalize_csv_post":{"properties":{"file":{"type":"string","format":"binary","title":"File","description":"CSV file containing transaction data."}},"type":"object","required":["file"],"title":"Body_normalize_csv_v1_normalize_csv_post"},"CSVUploadResponse":{"properties":{"status":{"type":"string","title":"Status","default":"success"},"filename":{"type":"string","title":"Filename"},"rows_parsed":{"type":"integer","title":"Rows Parsed"},"rows_normalized":{"type":"integer","title":"Rows Normalized"},"column_used":{"type":"string","title":"Column Used","description":"The CSV column that was auto-detected for transaction strings."},"data":{"items":{"$ref":"#/components/schemas/NormalizedTransaction"},"type":"array","title":"Data"}},"type":"object","required":["filename","rows_parsed","rows_normalized","column_used","data"],"title":"CSVUploadResponse","description":"Response for the CSV upload endpoint."},"ContactRequest":{"properties":{"name":{"type":"string","title":"Name"},"email":{"type":"string","format":"email","title":"Email"},"message":{"type":"string","maxLength":2000,"title":"Message"}},"type":"object","required":["name","email","message"],"title":"ContactRequest"},"ContactResponse":{"properties":{"status":{"type":"string","title":"Status","default":"sent"}},"type":"object","title":"ContactResponse"},"DemoRequest":{"properties":{"transaction":{"type":"string","maxLength":1024,"minLength":1,"title":"Transaction","description":"A raw transaction string to normalize (e.g. 'UPI/ZOMATO/98123456/FOOD').","examples":["UPI/ZOMATO/98123456/FOOD"]}},"type":"object","required":["transaction"],"title":"DemoRequest","description":"Public demo request — accepts a single raw transaction string."},"ExplainField":{"properties":{"path":{"type":"string","enum":["keyword_match","gemini_fallback","unmatched"],"title":"Path","description":"The classification path taken: keyword_match, gemini_fallback, or unmatched."},"rule":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rule","description":"The keyword rule that fired, e.g. 'zomato → Food and Beverage'. None for Gemini/unmatched."},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model","description":"The LLM model used, e.g. 'gemini-2.0-flash'. None for keyword_match/unmatched."},"confidence":{"type":"number","maximum":1.0,"minimum":0.0,"title":"Confidence","description":"Confidence score: 0.95 (keyword start), 0.85 (keyword found), 0.65 (Gemini), 0.10 (unmatched)."},"ruleset_version":{"type":"string","title":"Ruleset Version","description":"Version of the keyword ruleset that was active when this result was produced."}},"type":"object","required":["path","confidence","ruleset_version"],"title":"ExplainField","description":"Explains exactly how a transaction was categorized.\n\nAgents can inspect this to understand the classification path,\nwhich ruleset version produced it, and how confident the result is."},"GenerateKeyRequest":{"properties":{"email":{"type":"string","maxLength":256,"minLength":3,"title":"Email","description":"Developer's email address. Used for Stripe customer creation and key identification.","examples":["dev@example.com"]}},"type":"object","required":["email"],"title":"GenerateKeyRequest","description":"Self-serve key generation request.\n\nAny developer can provide their email to get an API key.\nA Stripe customer is created automatically."},"GenerateKeyResponse":{"properties":{"status":{"type":"string","title":"Status","default":"created"},"api_key":{"type":"string","title":"Api Key","description":"The raw API key. Store it securely — shown only once."},"key_prefix":{"type":"string","title":"Key Prefix","description":"The key prefix for identification."},"email":{"type":"string","title":"Email"},"stripe_customer_id":{"type":"string","title":"Stripe Customer Id","description":"The Stripe Customer ID created for billing.","default":""}},"type":"object","required":["api_key","key_prefix","email"],"title":"GenerateKeyResponse","description":"Response after self-serve key generation.\n\nThe raw key is returned ONLY in this response — it cannot be retrieved later."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HealthResponse":{"properties":{"status":{"type":"string","title":"Status","default":"healthy"},"version":{"type":"string","title":"Version"},"service":{"type":"string","title":"Service","default":"financial-data-normalizer"}},"type":"object","required":["version"],"title":"HealthResponse","description":"Health-check response payload."},"KeyInfo":{"properties":{"id":{"type":"integer","title":"Id"},"key_prefix":{"type":"string","title":"Key Prefix"},"label":{"type":"string","title":"Label"},"email":{"type":"string","title":"Email","default":""},"stripe_customer_id":{"type":"string","title":"Stripe Customer Id"},"is_active":{"type":"boolean","title":"Is Active"},"is_paid":{"type":"boolean","title":"Is Paid","default":false},"created_at":{"type":"string","title":"Created At"},"last_used_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Used At"}},"type":"object","required":["id","key_prefix","label","stripe_customer_id","is_active","created_at","last_used_at"],"title":"KeyInfo","description":"Public info about an API key (never includes the hash or raw key)."},"ListKeysResponse":{"properties":{"status":{"type":"string","title":"Status","default":"success"},"keys":{"items":{"$ref":"#/components/schemas/KeyInfo"},"type":"array","title":"Keys"}},"type":"object","required":["keys"],"title":"ListKeysResponse","description":"Response listing all API keys."},"NormalizedTransaction":{"properties":{"raw_string":{"type":"string","title":"Raw String","description":"The original raw transaction string."},"clean_merchant":{"type":"string","title":"Clean Merchant","description":"Cleaned and standardized merchant name."},"category":{"type":"string","title":"Category","description":"Standardized spending category."},"transaction_type":{"$ref":"#/components/schemas/TransactionType","description":"Direction of the transaction: debit, credit, or unknown."},"channel":{"$ref":"#/components/schemas/PaymentChannel","description":"Payment channel / instrument (UPI, NEFT, POS, etc.)."},"normalized_timestamp":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Normalized Timestamp","description":"ISO-8601 timestamp extracted from the raw string, or null."},"ruleset_version":{"type":"string","title":"Ruleset Version","description":"Version of the keyword ruleset used for classification."},"is_reversal":{"type":"boolean","title":"Is Reversal","description":"True if the transaction is a refund, reversal, or chargeback.","default":false},"is_partial":{"type":"boolean","title":"Is Partial","description":"True if the transaction is a split or partial payment.","default":false},"explain":{"$ref":"#/components/schemas/ExplainField","description":"Explains the classification path, matched rule, model used, and confidence."}},"type":"object","required":["raw_string","clean_merchant","category","transaction_type","channel","ruleset_version","explain"],"title":"NormalizedTransaction","description":"The canonical normalized output for a single transaction.\n\nFields:\n    raw_string:            The original input, echoed back for traceability.\n    clean_merchant:        The cleaned, human-readable merchant or entity name.\n    category:              A standardized spending category.\n    transaction_type:      Whether the transaction is a debit, credit, or unknown.\n    channel:               Payment channel extracted from the raw string.\n    normalized_timestamp:  ISO-8601 timestamp parsed from the raw string, if found.\n    ruleset_version:       Version of the keyword ruleset used for classification.\n    explain:               Nested object explaining the classification path and confidence."},"OTPSentResponse":{"properties":{"status":{"type":"string","title":"Status","default":"otp_sent"},"message":{"type":"string","title":"Message","default":"Check your email for a 6-digit verification code."}},"type":"object","title":"OTPSentResponse"},"PaymentChannel":{"type":"string","enum":["upi","neft","imps","rtgs","pos","atm","nach","ecs","cheque","card","internet_banking","mobile_banking","unknown"],"title":"PaymentChannel","description":"The payment channel / instrument extracted from the raw string."},"RevokeKeyRequest":{"properties":{"key_prefix":{"type":"string","title":"Key Prefix","description":"The prefix of the key to revoke (first 14 chars)."}},"type":"object","required":["key_prefix"],"title":"RevokeKeyRequest","description":"Request body for revoking an API key."},"SingleTransactionRequest":{"properties":{"raw_string":{"type":"string","maxLength":1024,"minLength":1,"title":"Raw String","description":"The raw, unprocessed transaction string from a bank statement.","examples":["UPI/ZOMATO/123456789/PAYMENT"]}},"type":"object","required":["raw_string"],"title":"SingleTransactionRequest","description":"Accepts a single raw financial transaction string for normalization.\n\nExample payload:\n    {\"raw_string\": \"UPI/ZOMATO/123456789/PAYMENT\"}"},"SingleTransactionResponse":{"properties":{"status":{"type":"string","title":"Status","default":"success"},"data":{"$ref":"#/components/schemas/NormalizedTransaction"}},"type":"object","required":["data"],"title":"SingleTransactionResponse","description":"Wraps a single normalized transaction result."},"TransactionType":{"type":"string","enum":["debit","credit","unknown"],"title":"TransactionType","description":"Enumeration of possible transaction directions."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VerifyOTPRequest":{"properties":{"email":{"type":"string","title":"Email","description":"The email address used in /generate."},"otp":{"type":"string","maxLength":6,"minLength":6,"title":"Otp","description":"The 6-digit OTP from your email."}},"type":"object","required":["email","otp"],"title":"VerifyOTPRequest"}},"securitySchemes":{"BearerAuth":{"type":"http","description":"Provide your API key (format: nyz_live_<hex>) as a Bearer token.","scheme":"bearer"}}}}