{"openapi":"3.1.0","info":{"title":"valuehub-proxy","version":"0.2.0","description":"Internal ELOVADE service. Receives authenticated webhooks and starts the matching workflows."},"servers":[{"url":"/"}],"tags":[{"name":"Webhooks","description":"Event intake. Maximum body size is 256 KB."},{"name":"Health","description":"For container healthchecks and monitoring."}],"paths":{"/webhooks/keycloak":{"post":{"tags":["Webhooks"],"summary":"Receive a Keycloak event","description":"Keycloak sends everything it emits, so anything not listed here is answered with `200`.\n\n| Event type | From |\n| --- | --- |\n| `user.created` | `REGISTER` |\n| `user.updated` | `UPDATE_PROFILE`, `UPDATE_EMAIL`, `VERIFY_EMAIL`, admin `UPDATE` on `USER` |\n| `user.deleted` | admin `DELETE` on `USER` |\n| `user.offboarded` | `DELETE_ACCOUNT`, admin `UPDATE` on `USER` with `enabled: false` |\n| `organization.created` | admin `CREATE` on `ORGANIZATION` |\n| `organization.updated` | admin `UPDATE` on `ORGANIZATION` |\n| `organization.deleted` | admin `DELETE` on `ORGANIZATION` |\n| `organization.member_added` | `INVITE_ORG`, admin `CREATE` on `ORGANIZATION_MEMBERSHIP` |\n| `organization.member_removed` | admin `DELETE` on `ORGANIZATION_MEMBERSHIP` |","operationId":"postKeycloakEvent","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeycloakEvent"},"example":{"id":"63bf418a-694e-4325-a168-6146a3acc2a8","time":1793874718741,"type":"UPDATE_PROFILE","realmName":"example","clientId":"account","userId":"32916298-7da6-470d-96d3-f4fd02a33bb4","error":null}}}},"responses":{"200":{"description":"Understood, but outside the routing table, or `not_implemented` while the service is not set up yet. Nothing is stored and nothing starts.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Ignored"},{"$ref":"#/components/schemas/NotImplemented"}]},"example":{"status":"ignored","eventId":"e-1"}}}},"202":{"description":"Stored, and the workflow will follow. This does not mean the workflow ran.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Accepted"},"example":{"status":"accepted","eventId":"e-1","eventType":"user.updated"}}}},"400":{"description":"Not JSON, or the event breaks the schema. Resending the same body does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_event"}}}},"401":{"description":"Missing or wrong token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"unauthorized"}}}},"413":{"description":"The body is larger than 256 KB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"payload_too_large"}}}},"500":{"description":"Unexpected error. The event may not be stored, so retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"internal_error"}}}},"503":{"description":"The event could not be stored. Retry later with the same id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"unavailable"}}},"headers":{"Retry-After":{"description":"Seconds to wait.","schema":{"type":"integer"}}}}}}},"/webhooks/zoho":{"post":{"tags":["Webhooks"],"summary":"Receive a Zoho event","description":"The contract for Zoho workflows. Send one event for every change.\n\n| `object` | `operation` | Event type |\n| --- | --- | --- |\n| `partner`, `customer` | `created`, `updated`, `deleted` | `organization.created`, `.updated`, `.deleted` |\n| `contact` | `created`, `updated`, `deleted` | `user.created`, `.updated`, `.deleted` |\n| `contact` | `linked`, `unlinked` | `organization.member_added`, `.member_removed` |\n\n`202` does not mean the workflow ran. With no answer or a timeout, retry with the same `eventId`: that is safe.","operationId":"postZohoEvent","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ZohoEvent"},"example":{"eventId":"Contacts-1234567890-2026-11-05T11:31:58+01:00","occurredAt":"2026-11-05T11:31:58+01:00","object":"contact","contactKind":"portaluser","operation":"updated","recordId":"1234567890","elUid":"con_4711","customerId":"100042","idpStatus":"IdPOrgUser","actor":"someone@example.com","data":{"First_Name":"John","Last_Name":"Doe","Email":"john@example.com"}}}}},"responses":{"200":{"description":"Understood, but outside the routing table. Nothing is stored and nothing starts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ignored"},"example":{"status":"ignored","eventId":"e-1"}}}},"202":{"description":"Stored, and the workflow will follow. This does not mean the workflow ran.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Accepted"},"example":{"status":"accepted","eventId":"e-1","eventType":"user.updated"}}}},"400":{"description":"Not JSON, or the event breaks the schema. Resending the same body does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_event"}}}},"401":{"description":"Missing or wrong token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"unauthorized"}}}},"404":{"description":"Zoho is not enabled on this instance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"not_found"}}}},"413":{"description":"The body is larger than 256 KB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"payload_too_large"}}}},"500":{"description":"Unexpected error. The event may not be stored, so retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"internal_error"}}}},"503":{"description":"The event could not be stored. Retry later with the same id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"unavailable"}}},"headers":{"Retry-After":{"description":"Seconds to wait.","schema":{"type":"integer"}}}}}}},"/webhooks/{service}":{"post":{"tags":["Webhooks"],"summary":"Receive an event from an internal service","description":"The contract for other internal services. Each service has its own route and token, and names the event type itself.\n\n`202` does not mean the workflow ran. With no answer or a timeout, retry with the same `eventId`: that is safe.","operationId":"postServiceEvent","security":[{"bearerAuth":[]}],"parameters":[{"name":"service","in":"path","required":true,"description":"The name agreed for your service.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceEvent"},"example":{"eventId":"user-1234567890-2026-11-05T11:31:58+01:00","occurredAt":"2026-11-05T11:31:58+01:00","eventType":"user.updated","entityId":"1234567890","elUid":"con_4711","actor":"someone@example.com","data":{"firstName":"John","lastName":"Doe","email":"john@example.com"}}}}},"responses":{"202":{"description":"Stored, and the workflow will follow. This does not mean the workflow ran.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Accepted"},"example":{"status":"accepted","eventId":"e-1","eventType":"user.updated"}}}},"400":{"description":"Not JSON, or the event breaks the schema. Resending the same body does not help.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"invalid_event"}}}},"401":{"description":"Missing or wrong token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"unauthorized"}}}},"404":{"description":"This service is not enabled on this instance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"not_found"}}}},"413":{"description":"The body is larger than 256 KB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"payload_too_large"}}}},"500":{"description":"Unexpected error. The event may not be stored, so retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"internal_error"}}}},"503":{"description":"The event could not be stored. Retry later with the same id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"unavailable"}}},"headers":{"Retry-After":{"description":"Seconds to wait.","schema":{"type":"integer"}}}}}}},"/healthz":{"get":{"tags":["Health"],"summary":"Liveness","description":"Touches no dependency.","operationId":"getHealth","responses":{"200":{"description":"The process is up.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"const":"ok"}},"required":["status"]}}}}}}},"/readyz":{"get":{"tags":["Health"],"summary":"Readiness","description":"Checks the workflow backend and the event store.","operationId":"getReadiness","responses":{"200":{"description":"Ready.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Readiness"}}}},"503":{"description":"A dependency is down, or the service is not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Readiness"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"One token per source and environment."}},"schemas":{"KeycloakEvent":{"type":"object","properties":{"id":{"type":"string","pattern":"^[A-Za-z0-9_.~:+-]{1,128}$"},"time":{"type":"integer","exclusiveMinimum":0,"maximum":8640000000000000,"description":"Milliseconds since the epoch."},"realmName":{"type":["string","null"]},"realmId":{"type":["string","null"]},"type":{"description":"User event type. An event needs this or `operationType`.","type":["string","null"]},"operationType":{"description":"Admin event operation. An event needs this or `type`.","type":["string","null"]},"resourceType":{"type":["string","null"]},"resourcePath":{"type":["string","null"]},"representation":{"description":"The changed resource, as a JSON string.","type":["string","null"]},"userId":{"type":["string","null"]},"clientId":{"type":["string","null"]},"authDetails":{"anyOf":[{"type":"object","properties":{"clientId":{"type":["string","null"]},"userId":{"type":["string","null"]}},"additionalProperties":{}},{"type":"null"}]},"details":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"error":{"description":"Events with an error are ignored.","type":["string","null"]}},"required":["id","time"],"additionalProperties":{}},"ZohoEvent":{"type":"object","properties":{"eventId":{"type":"string","pattern":"^[A-Za-z0-9_.~:+-]{1,128}$","description":"Unique per change, and the same if the same change is sent twice. Build it as `<module>-<record id>-<Modified_Time>`."},"occurredAt":{"type":"string","format":"date-time","description":"ISO 8601 with time zone."},"object":{"type":"string","enum":["partner","customer","contact"]},"contactKind":{"description":"Required for contacts. Must not be set on partners and customers.","anyOf":[{"type":"string","enum":["user","portaluser"]},{"type":"null"}]},"operation":{"type":"string","enum":["created","updated","deleted","linked","unlinked"],"description":"`linked` and `unlinked` are allowed for contacts only."},"recordId":{"type":"string","minLength":1,"description":"The record's Zoho id."},"elUid":{"description":"The record's global EL_UID, the key shared by all systems. Required, except for `created`.","anyOf":[{"anyOf":[{"type":"string","minLength":1},{"type":"integer"}]},{"type":"null"}]},"accountElUid":{"description":"The account's global EL_UID. Required for `linked` and `unlinked`.","anyOf":[{"anyOf":[{"type":"string","minLength":1},{"type":"integer"}]},{"type":"null"}]},"customerId":{"description":"The UM CustomerID, a field of the organization. Required for partners and customers, and needed for B2B contacts.","anyOf":[{"anyOf":[{"type":"string","minLength":1},{"type":"integer"}]},{"type":"null"}]},"idpStatus":{"description":"For B2B contacts.","anyOf":[{"type":"string","enum":["IdPOrgAdmin","IdPOrgUser","NoIdPAccess"]},{"type":"null"}]},"accountId":{"description":"Required for `linked` and `unlinked`.","anyOf":[{"type":"string","minLength":1},{"type":"null"}]},"accountType":{"description":"Required for `linked` and `unlinked`.","anyOf":[{"type":"string","enum":["partner","customer"]},{"type":"null"}]},"actor":{"description":"Who made the change. Recommended, because it is what prevents loops.","type":["string","null"]},"data":{"description":"The complete record, not only the changed fields. Required for `created` and `updated`.","anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]}},"required":["eventId","occurredAt","object","operation","recordId"],"additionalProperties":{}},"ServiceEvent":{"type":"object","properties":{"eventId":{"type":"string","pattern":"^[A-Za-z0-9_.~:+-]{1,128}$","description":"Unique per change, and the same if the same change is sent twice."},"occurredAt":{"type":"string","format":"date-time","description":"ISO 8601 with time zone."},"eventType":{"type":"string","enum":["user.created","user.updated","user.deleted","organization.created","organization.updated","organization.deleted","organization.member_added","organization.member_removed"]},"entityId":{"type":"string","minLength":1,"description":"The user's or organization's id in the sending service. For `organization.member_*`, the organization."},"elUid":{"description":"The global EL_UID of the user or organization, the key shared by all systems. Required, except for `*.created`.","anyOf":[{"anyOf":[{"type":"string","minLength":1},{"type":"integer"}]},{"type":"null"}]},"customerId":{"description":"The UM CustomerID, a field of the organization. Required for `organization.*`.","anyOf":[{"anyOf":[{"type":"string","minLength":1},{"type":"integer"}]},{"type":"null"}]},"actor":{"description":"Who made the change.","type":["string","null"]},"member":{"description":"The user concerned. Required for `organization.member_*`.","anyOf":[{"type":"object","properties":{"userId":{"type":"string","minLength":1},"elUid":{"anyOf":[{"type":"string","minLength":1},{"type":"integer"}]},"email":{"type":["string","null"]},"username":{"type":["string","null"]}},"required":["userId","elUid"]},{"type":"null"}]},"data":{"description":"The complete record, not only the changed fields. Required for `*.created` and `*.updated`.","anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]}},"required":["eventId","occurredAt","eventType","entityId"],"additionalProperties":{}},"Accepted":{"type":"object","properties":{"status":{"const":"accepted"},"eventId":{"type":"string"},"eventType":{"type":"string","enum":["user.created","user.updated","user.deleted","user.offboarded","organization.created","organization.updated","organization.deleted","organization.member_added","organization.member_removed"]}},"required":["status","eventId","eventType"]},"Ignored":{"type":"object","properties":{"status":{"const":"ignored"},"eventId":{"type":"string"}},"required":["status","eventId"]},"NotImplemented":{"type":"object","properties":{"status":{"const":"not_implemented"},"message":{"type":"string"}},"required":["status","message"]},"Error":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"]},"Readiness":{"type":"object","properties":{"status":{"type":"string","enum":["ready","not_ready","not_configured"]},"airflow":{"type":"boolean","description":"The workflow backend is reachable."},"spoolWritable":{"type":"boolean","description":"Events can be stored."},"pending":{"type":"integer","description":"Events waiting for delivery."}},"required":["status"]}}}}