{"openapi":"3.0.0","paths":{"/api/v1/submissions":{"post":{"description":"Called from a browser. No authentication: the request is checked against the site's allowed origins and, if the site has CAPTCHA configured, its token is verified.\n\nA 201 means the submission was stored and its notification jobs were queued — not that anything has been delivered yet. Delivery runs afterwards with its own retries.","operationId":"SubmissionController_create","parameters":[{"name":"idempotency-key","required":true,"in":"header","schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","required":false,"description":"Optional. 8–255 characters. Replays the first response for a repeated request instead of performing the work again. Reusing a key with a different body returns 422.","schema":{"type":"string","minLength":8,"maxLength":255}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSubmissionDto"}}}},"responses":{"201":{"description":"Stored and queued.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"submissionId":{"type":"string","format":"uuid","example":"9f3c21e4-6a1b-4e58-9a2f-2c1d7e4b8a10"}}}}}},"400":{"description":"Validation failed, a CAPTCHA token is required, or CAPTCHA verification failed."},"404":{"description":"Submission target not found or not permitted. Returned for an unknown site key, an unknown form key, an inactive site or form, AND for an Origin that is not on the allowlist — deliberately identical in all four cases so the endpoint cannot be used to discover valid keys."},"422":{"description":"The Idempotency-Key has already been used with a different request body."},"429":{"description":"Rate limit: 10 requests per minute per IP."},"503":{"description":"CAPTCHA is misconfigured for the site, or the CAPTCHA provider could not be reached."}},"summary":"Submit a form","tags":["Submissions"]}},"/api/v1/notifications":{"post":{"description":"Authenticated with a site API key in the `x-api-key` header.\n\nName a `formKey` alone and the form's configured recipients and webhooks are used. Name `recipients` explicitly and every address must ALREADY be an active recipient on that form — otherwise a leaked key would be an open relay, since the caller also controls the subject and body.\n\nA 201 means stored and queued, not delivered.","operationId":"NotificationController_create","parameters":[{"name":"idempotency-key","required":true,"in":"header","schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","required":false,"description":"Optional. 8–255 characters. Replays the first response for a repeated request instead of performing the work again. Reusing a key with a different body returns 422.","schema":{"type":"string","minLength":8,"maxLength":255}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateNotificationDto"}}}},"responses":{"201":{"description":"Stored and queued.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true},"submissionId":{"type":"string","format":"uuid"}}}}}},"401":{"description":"Missing or invalid API key."},"403":{"description":"The API key belongs to a different site, or a named recipient is not configured on that form. The response does not echo the address back."},"404":{"description":"The site or the formKey does not resolve to an active record."},"422":{"description":"The Idempotency-Key has already been used with a different request body."}},"security":[{"api-key":[]}],"summary":"Send a notification from your backend","tags":["Notifications"]}},"/api/v1/tenant-discovery":{"post":{"operationId":"TenantDiscoveryController_discover","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Optional. 8–255 characters. Replays the first response for a repeated request instead of performing the work again. Reusing a key with a different body returns 422.","schema":{"type":"string","minLength":8,"maxLength":255}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DiscoverTenantDto"}}}},"responses":{"200":{"description":""}},"tags":["TenantDiscovery"]}}},"info":{"title":"Expeed Relay API","description":"Send notifications from a website form or from your own backend.\n\nTwo endpoints:\n\n- `POST /submissions` — public, called from a browser. Protected by the site's origin allowlist and, if configured, CAPTCHA.\n- `POST /notifications` — authenticated with an API key, called from your server.\n\nBoth accept an optional `Idempotency-Key` header. Send the same key with the same body and the first response is replayed rather than the work running twice — which is what you want when a request times out and your client retries.","version":"1.0.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"api-key":{"type":"apiKey","in":"header","name":"x-api-key","description":"Site API key, issued in the admin under API keys. Server-side only."}},"schemas":{"CreateSubmissionDto":{"type":"object","properties":{}},"CreateNotificationDto":{"type":"object","properties":{}},"OnboardTenantDto":{"type":"object","properties":{}},"UpdateTenantDto":{"type":"object","properties":{}},"DiscoverTenantDto":{"type":"object","properties":{}},"CreateSiteDto":{"type":"object","properties":{}},"UpdateSiteDto":{"type":"object","properties":{}},"CreateProviderConfigDto":{"type":"object","properties":{}},"CreateFormDto":{"type":"object","properties":{}},"UpdateFormDto":{"type":"object","properties":{}},"CreateRecipientDto":{"type":"object","properties":{}},"UpdateRecipientDto":{"type":"object","properties":{}},"PreviewTemplateDto":{"type":"object","properties":{}},"CreateTemplateDto":{"type":"object","properties":{}},"CreateWebhookDto":{"type":"object","properties":{}},"UpdateWebhookDto":{"type":"object","properties":{}},"CreateUserDto":{"type":"object","properties":{}},"UpdateUserDto":{"type":"object","properties":{}},"CreateApiKeyDto":{"type":"object","properties":{}},"ClaimHandoverDto":{"type":"object","properties":{}}}}}