{"openapi":"3.1.0","info":{"title":"WagerLink API","version":"1.0.0","summary":"Public API and webhook documentation for WagerLink.","description":"WagerLink provides API endpoints for wagering operators to sync customer data and customer notes into the WagerLink platform.\n\nWagerLink can also send outbound webhooks to your configured endpoint when important compliance or preference events occur, including BetStop matches and opt-in/opt-out changes.\n\nPublic API endpoints use a private API key in the `Authorization` header:\n\n```http\nAuthorization: Bearer <PRIVATE_API_KEY>\n```\n"},"servers":[{"url":"https://YOUR-WAGERLINK-WEB-DOMAIN","description":"Production web application API"},{"url":"http://localhost:3000","description":"Local development API"}],"security":[{"bearerPrivateApiKey":[]}],"tags":[{"name":"Customers","description":"Sync customer records into WagerLink."},{"name":"Customer Notes","description":"Create or update external customer notes."},{"name":"System","description":"System-level routes used by platform integrations."},{"name":"Webhooks","description":"Outbound webhook events sent by WagerLink to your configured endpoint."}],"paths":{"/api/contacts":{"post":{"tags":["Customers"],"operationId":"upsertCustomer","summary":"Create or update a customer","description":"Creates or updates a customer in WagerLink.\n\nIf `external_id` is provided and a customer in the same organization already has that external ID, the existing customer is updated. Otherwise, WagerLink creates a new customer.\n\nFor idempotent syncing, always send a stable `external_id`.\n","security":[{"bearerPrivateApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerUpsertRequest"},"examples":{"minimum":{"summary":"Minimum recommended wagering customer","value":{"external_id":"CUST-10001","first_name":"Ava","last_name":"Nguyen","dob":"1990-04-21","email":"ava.nguyen@example.com","phone":"+61400111222","post_code":"2000"}},"full":{"summary":"Full customer example","value":{"external_id":"CUST-10001","first_name":"Ava","last_name":"Nguyen","dob":"1990-04-21","email":"ava.nguyen@example.com","phone":"+61400111222","post_code":"2000","country":"AU","account_status":"active","sms_optin":true,"email_optin":true,"phone_optin":false,"other_optin":false,"balance":120.5,"total_deposit":850,"custom_attributes":{"vipTier":"gold","sourceSystem":"crm"}}}}}}},"responses":{"200":{"description":"Customer created or updated successfully.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerUpsertResponse"},"examples":{"created":{"summary":"Created","value":{"success":true,"action":"created","customerId":"j57xxxxxxxxxxxxxxxxxxxx"}},"updated":{"summary":"Updated","value":{"success":true,"action":"updated","customerId":"j57xxxxxxxxxxxxxxxxxxxx"}}}}}},"400":{"description":"Invalid JSON request body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Invalid JSON body"}}}},"401":{"description":"Missing bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Unauthorized - Missing Bearer token"}}}},"500":{"description":"Backend error or invalid private key handled by backend.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Internal Server Error"}}}}}}},"/api/notes":{"post":{"tags":["Customer Notes"],"operationId":"upsertCustomerNote","summary":"Create or update a customer note","description":"Creates or updates a customer note from an external system.\n\nIf `note_id` is supplied and a note with that external ID already exists for the organization, WagerLink updates that note. Otherwise, WagerLink creates a new note.\n\nIf `customer_id` is supplied, it must match the customer's external ID in WagerLink.\n","security":[{"bearerPrivateApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerNoteUpsertRequest"},"examples":{"minimum":{"summary":"Minimum note","value":{"note":"Customer requested a review of bonus eligibility."}},"full":{"summary":"Note linked to an external customer","value":{"customer_id":"CUST-10001","note_id":"NOTE-90001","note":"Customer requested a review of bonus eligibility.","noteType":"External","events":"RG,AML","created_by":"external-crm","status":"open"}}}}}},"responses":{"200":{"description":"Note created or updated successfully.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerNoteUpsertResponse"},"examples":{"created":{"summary":"Created","value":{"success":true,"action":"created","noteId":"k17xxxxxxxxxxxxxxxxxxxx"}},"updated":{"summary":"Updated","value":{"success":true,"action":"updated","noteId":"k17xxxxxxxxxxxxxxxxxxxx"}}}}}},"400":{"description":"Invalid JSON request body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Invalid JSON body"}}}},"401":{"description":"Missing bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Unauthorized - Missing Bearer token"}}}},"403":{"description":"Invalid private key or unauthorized note/customer access.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"invalidKey":{"summary":"Invalid private key","value":{"error":"Unauthorized: Invalid private key"}},"unauthorizedNote":{"summary":"Note belongs to another organization","value":{"error":"Unauthorized: Note does not belong to this organization."}}}}}},"500":{"description":"Backend error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"Customer with external ID CUST-10001 not found."}}}}}}},"/clerk-webhook":{"post":{"tags":["System"],"operationId":"clerkSubscriptionWebhook","summary":"Clerk subscription webhook","description":"System webhook receiver used by Clerk/Svix. This endpoint is not intended for customer API use.\n\nThe route verifies `svix-id`, `svix-timestamp`, and `svix-signature` headers using the Clerk webhook secret.\n","security":[],"parameters":[{"name":"svix-id","in":"header","required":true,"schema":{"type":"string"}},{"name":"svix-timestamp","in":"header","required":true,"schema":{"type":"string"}},{"name":"svix-signature","in":"header","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClerkSubscriptionUpdatedEvent"},"example":{"type":"subscription.updated","data":{"status":"active","payer":{"organization_id":"org_xxxxxxxxx"}}}}}},"responses":{"200":{"description":"Event accepted."},"400":{"description":"Invalid signature, invalid event, or missing organization ID.","content":{"text/plain":{"examples":{"verificationFailed":{"value":"Error occured"},"missingOrganization":{"value":"Missing Organziation ID"}}}}}}}},"/verifyDomain":{"post":{"tags":["System"],"operationId":"verifyDomainTxtRecord","summary":"Verify DNS TXT record","description":"Verifies whether a DNS TXT record contains a provided token.\n\nThis route is used by domain verification flows. Confirm the deployed Convex HTTP route before publishing this externally.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyDomainRequest"},"example":{"txtHost":"_wagerlink.example.com","token":"wagerlink-verification-token"}}}},"responses":{"200":{"description":"Verification result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyDomainResponse"},"examples":{"verified":{"value":{"verified":true}},"failed":{"value":{"verified":false,"error":"DNS lookup failed"}}}}}}}}}},"webhooks":{"webhookTest":{"post":{"tags":["Webhooks"],"summary":"Webhook test event","description":"Sent when a user tests the webhook from the dashboard.","security":[{"webhookBasicAuth":[]},{"webhookBearerAuth":[]},{"webhookApiKey":[]},{}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookTestEvent"},"example":{"type":"webhook-test","message":"Webhook test message","timestamp":1717372800000}}}},"responses":{"200":{"description":"Your endpoint should return any 2xx response when accepted."}}}},"betstopCampaignMatch":{"post":{"tags":["Webhooks"],"summary":"BetStop match during campaign send","description":"Sent when a customer is identified as an active BetStop match during campaign processing.\n\nThis operational webhook is sent as a JSON array.\n","security":[{"webhookBasicAuth":[]},{"webhookBearerAuth":[]},{"webhookApiKey":[]},{}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BetStopCampaignMatchEvent"}},"example":[{"customer_id":"j57xxxxxxxxxxxxxxxxxxxx","type":"betstop","match_found":true}]}}},"responses":{"200":{"description":"Your endpoint should return any 2xx response when accepted."}}}},"betstopTestCheck":{"post":{"tags":["Webhooks"],"summary":"BetStop test check event","description":"Sent when the BetStop test action is run and produces a valid match result.","security":[{"webhookBasicAuth":[]},{"webhookBearerAuth":[]},{"webhookApiKey":[]},{}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BetStopTestCheckEvent"}},"example":[{"customer_id":"CUST-10001","betstop_found":true}]}}},"responses":{"200":{"description":"Your endpoint should return any 2xx response when accepted."}}}},"betstopBatch":{"post":{"tags":["Webhooks"],"summary":"BetStop batch event","description":"Sent by the scheduled/background BetStop batch process when matched customers are found.","security":[{"webhookBasicAuth":[]},{"webhookBearerAuth":[]},{"webhookApiKey":[]},{}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BetStopBatchEvent"},"example":{"type":"betstop-batch","payload":[{"customer_id":"CUST-10001","match_found":true}]}}}},"responses":{"200":{"description":"Your endpoint should return any 2xx response when accepted."}}}},"optInOutPreferenceChange":{"post":{"tags":["Webhooks"],"summary":"Opt-in or opt-out preference change","description":"Sent when a customer updates preferences through an opt-out link or when an operator changes opt status in the dashboard.\n\nThis operational webhook is sent as a JSON array.\n","security":[{"webhookBasicAuth":[]},{"webhookBearerAuth":[]},{"webhookApiKey":[]},{}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OptInOutPreferenceChangeEvent"}},"examples":{"optOutLink":{"summary":"Preference change from opt-out link","value":[{"customer_id":"CUST-10001","emailOpt":false,"smsOpt":false,"phoneOpt":true,"otherOpt":true}]},"dashboardToggle":{"summary":"Preference change from dashboard","value":[{"customer_id":"CUST-10001","smsOpt":false,"phoneOpt":true,"emailOpt":false,"otherOpt":true,"type":"opt-in-out"}]}}}}},"responses":{"200":{"description":"Your endpoint should return any 2xx response when accepted."}}}}},"components":{"securitySchemes":{"bearerPrivateApiKey":{"type":"http","scheme":"bearer","description":"Private API key generated/configured for the organization's API plugin."},"webhookBasicAuth":{"type":"http","scheme":"basic","description":"Optional webhook authentication mode configured in WagerLink."},"webhookBearerAuth":{"type":"http","scheme":"bearer","description":"Optional webhook bearer authentication mode configured in WagerLink."},"webhookApiKey":{"type":"apiKey","in":"header","name":"x-api-key","description":"Optional webhook API key authentication mode configured in WagerLink."}},"schemas":{"CustomerUpsertRequest":{"type":"object","additionalProperties":true,"description":"Customer payload accepted by `POST /api/contacts`.\n\nOnly known fields are mapped into WagerLink customer fields. Unknown fields are ignored unless included in `custom_attributes`.\n","properties":{"external_id":{"type":"string","description":"Recommended external customer ID. Used for idempotent update matching."},"customerId":{"type":"string","description":"Alternate customer ID field."},"role":{"type":"string"},"first_name":{"type":"string"},"middle_name":{"type":"string"},"last_name":{"type":"string"},"dob":{"type":"string","description":"Recommended for wagering/BetStop flows. Use YYYY-MM-DD.","examples":["1990-04-21"]},"email":{"type":"string","format":"email"},"phone":{"type":"string","examples":["+61400111222"]},"verified":{"type":"boolean"},"is_selfExcluded":{"type":"boolean"},"bet_stop":{"type":"boolean"},"classification":{"type":"string"},"override_classification":{"type":"string"},"address1":{"type":"string"},"address2":{"type":"string"},"city":{"type":"string","description":"Maps to WagerLink suburb."},"region":{"type":"string","description":"Maps to WagerLink state."},"post_code":{"type":"string","description":"Recommended for wagering/BetStop flows."},"country":{"type":"string","examples":["AU"]},"custom_attributes":{"type":"object","additionalProperties":true},"sms_optin":{"type":"boolean"},"email_optin":{"type":"boolean"},"phone_optin":{"type":"boolean"},"other_optin":{"type":"boolean"},"deposit_number":{"type":"number"},"total_deposit":{"type":"number"},"balance":{"type":"number"},"bonus_balance":{"type":"number"},"redeem_bonus_balance":{"type":"number"},"withdrawn":{"type":"number"},"bet_count":{"type":"number"},"avg_stake":{"type":"number"},"free_bet_count":{"type":"number"},"turnover":{"type":"number"},"win_loss":{"type":"number"},"pot":{"type":"number"},"deposit_limit_daily":{"type":"number"},"deposit_limit_weekly":{"type":"number"},"deposit_limit_monthly":{"type":"number"},"blocked":{"type":"boolean","description":"If true, status becomes Blocked."},"is_locked":{"type":"boolean","description":"If true, status becomes Blocked."},"account_status":{"type":"string","enum":["active","deactive"],"description":"`active` maps to `Active`; `deactive` maps to `Deactivated`."},"signed_up_at":{"type":"string","description":"Optional timestamp/date from source system."},"last_logged_in_at":{"type":"string","description":"Optional timestamp/date from source system."}}},"CustomerUpsertResponse":{"type":"object","required":["success","action","customerId"],"properties":{"success":{"type":"boolean","const":true},"action":{"type":"string","enum":["created","updated"]},"customerId":{"type":"string","description":"WagerLink internal customer document ID."}}},"CustomerNoteUpsertRequest":{"type":"object","required":["note"],"additionalProperties":false,"properties":{"customer_id":{"type":"string","description":"External customer ID. Must match the customer external_id sent to /api/contacts."},"contactSessionsId":{"type":"string","description":"Internal WagerLink contact session ID, if known."},"note":{"type":"string","description":"Note content."},"noteType":{"type":"string","description":"Note category/type label."},"events":{"type":"string","description":"Comma-separated risk types. Valid values are AML, ECDD, Fraud, Alerts, BetNote, RG.","examples":["RG,AML"]},"note_id":{"type":"string","description":"External note ID. Used for idempotent updates."},"created_by":{"type":"string","description":"External author/source name."},"status":{"type":"string","description":"Status from external system."}}},"CustomerNoteUpsertResponse":{"type":"object","required":["success","action","noteId"],"properties":{"success":{"type":"boolean","const":true},"action":{"type":"string","enum":["created","updated"]},"noteId":{"type":"string","description":"WagerLink internal customer note document ID."}}},"ErrorResponse":{"type":"object","required":["error"],"properties":{"error":{"type":"string"}}},"ClerkSubscriptionUpdatedEvent":{"type":"object","required":["type","data"],"properties":{"type":{"type":"string","const":"subscription.updated"},"data":{"type":"object","required":["status"],"properties":{"status":{"type":"string","examples":["active"]},"payer":{"type":"object","properties":{"organization_id":{"type":"string"}}}}}}},"VerifyDomainRequest":{"type":"object","required":["txtHost","token"],"properties":{"txtHost":{"type":"string"},"token":{"type":"string"}}},"VerifyDomainResponse":{"type":"object","required":["verified"],"properties":{"verified":{"type":"boolean"},"error":{"type":"string"}}},"WebhookTestEvent":{"type":"object","required":["type","message","timestamp"],"properties":{"type":{"type":"string","const":"webhook-test"},"message":{"type":"string"},"timestamp":{"type":"number","description":"Unix timestamp in milliseconds."}}},"BetStopCampaignMatchEvent":{"type":"object","required":["customer_id","type","match_found"],"properties":{"customer_id":{"type":"string","description":"Customer identifier for the matched customer. This path may send the internal WagerLink customer ID."},"type":{"type":"string","const":"betstop"},"match_found":{"type":"boolean"}}},"BetStopTestCheckEvent":{"type":"object","properties":{"customer_id":{"type":"string","description":"Customer ID returned by the BetStop result, when available."},"betstop_found":{"type":"boolean"}}},"BetStopBatchEvent":{"type":"object","required":["type","payload"],"properties":{"type":{"type":"string","const":"betstop-batch"},"payload":{"type":"array","description":"List of matched customer records from the batch process.","items":{"type":"object","additionalProperties":true}}}},"OptInOutPreferenceChangeEvent":{"type":"object","properties":{"customer_id":{"type":"string","description":"External customer ID when available."},"smsOpt":{"type":"boolean"},"emailOpt":{"type":"boolean"},"phoneOpt":{"type":"boolean"},"otherOpt":{"type":"boolean"},"type":{"type":"string","enum":["opt-in-out"]}}}}}}