{"_id":"@arthurzakharov/convert-service","_rev":"3-f73581b38a3e39838e01b11922832809","name":"@arthurzakharov/convert-service","dist-tags":{"latest":"1.0.2"},"versions":{"1.0.0":{"name":"@arthurzakharov/convert-service","version":"1.0.0","author":{"name":"Artur Zakharov","email":"zakharov.arthur@icloud.com"},"_id":"@arthurzakharov/convert-service@1.0.0","maintainers":[{"name":"arthurzakharov","email":"zakharov.arthur@icloud.com"}],"homepage":"https://github.com/arthurzakharov/convert-service#readme","bugs":{"url":"https://github.com/arthurzakharov/convert-service/issues"},"dist":{"shasum":"965788fdaa74abc817b23e0db7761a02a6c02641","tarball":"https://registry.npmjs.org/@arthurzakharov/convert-service/-/convert-service-1.0.0.tgz","fileCount":54,"integrity":"sha512-QF9FGu/jDEarJ6RG0Gy5LzV8A0qmHg/W89nwg6COoCrp0jSXJp5oAVxfVROkivQLi33Os9GfFk0U7/5VNAVqbg==","signatures":[{"sig":"MEYCIQCzDrqVNTTbzEMPRmX1PHuazgY5YzJMuXlH0py8KrVkXAIhALnppiVhJmvNPyoUDlLvxjy4i3HqKbPJUOZzf5+JbFJ3","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":122595},"main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./client":{"types":"./dist/client.d.ts","default":"./dist/client.js"},"./contracts":{"types":"./dist/contracts.d.ts","default":"./dist/contracts.js"}},"gitHead":"3cab3580f9e17efbcc13e5a14c3e37001a640316","scripts":{"dev":"ts-node-dev -r tsconfig-paths/register --respawn --transpile-only src/index.ts","build":"tsc && tsc-alias && node scripts/build-report.js","start":"node dist/index.js","typecheck":"tsc --noEmit"},"_npmUser":{"name":"arthurzakharov","email":"zakharov.arthur@icloud.com"},"repository":{"url":"git+https://github.com/arthurzakharov/convert-service.git"},"_npmVersion":"11.6.2","description":"Server-side Convert.com SDK proxy service","directories":{},"_nodeVersion":"24.11.1","dependencies":{"cors":"^2.8.5","dotenv":"^16.4.5","express":"^4.19.2","geoip-lite":"^1.4.10","ua-parser-js":"^2.0.10","@convertcom/js-sdk":"^4.4.3"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsc-alias":"^1.8.17","typescript":"^5.4.5","@types/cors":"^2.8.17","@types/node":"^20.14.0","ts-node-dev":"^2.0.0","@types/express":"^4.17.21","tsconfig-paths":"^4.2.0","@types/geoip-lite":"^1.4.4","@types/ua-parser-js":"^0.7.39"},"_npmOperationalInternal":{"tmp":"tmp/convert-service_1.0.0_1781288026393_0.8308327446666619","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@arthurzakharov/convert-service","version":"1.0.1","author":{"name":"Artur Zakharov","email":"zakharov.arthur@icloud.com"},"_id":"@arthurzakharov/convert-service@1.0.1","maintainers":[{"name":"arthurzakharov","email":"zakharov.arthur@icloud.com"}],"homepage":"https://github.com/arthurzakharov/convert-service#readme","bugs":{"url":"https://github.com/arthurzakharov/convert-service/issues"},"dist":{"shasum":"506573d603e501895d3602b1129957fb4041c9a7","tarball":"https://registry.npmjs.org/@arthurzakharov/convert-service/-/convert-service-1.0.1.tgz","fileCount":54,"integrity":"sha512-Q//w8/eoSv2xq1hCxSOf9+9NCMDobTnHhh1MnJmnW4ui2ZTEuWLe+bKf2ugYr+GisWqdkbqkvYnqOshaDKGgNg==","signatures":[{"sig":"MEQCIB40KLhyrDtVdo9j2I5geNuuz6w7vMsXyHniesBLgrbWAiBdw/KbpN8twq4k/jNui8WwUTJagt2s+dteOgrVjH/Cig==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":122594},"main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./client":{"types":"./dist/client.d.ts","default":"./dist/client.js"},"./contracts":{"types":"./dist/contracts.d.ts","default":"./dist/contracts.js"}},"gitHead":"e84b96a5e6175bc2264cab5e0eb5486bf167b407","scripts":{"dev":"ts-node-dev -r tsconfig-paths/register --respawn --transpile-only src/index.ts","build":"tsc && tsc-alias && node scripts/build-report.js","start":"node dist/index.js","typecheck":"tsc --noEmit"},"_npmUser":{"name":"arthurzakharov","email":"zakharov.arthur@icloud.com"},"repository":{"url":"git+https://github.com/arthurzakharov/convert-service.git"},"_npmVersion":"11.6.2","description":"Server-side Convert.com SDK proxy service","directories":{},"_nodeVersion":"24.11.1","dependencies":{"cors":"^2.8.5","dotenv":"^16.4.5","express":"^4.19.2","geoip-lite":"^2.0.2","ua-parser-js":"^2.0.10","@convertcom/js-sdk":"^4.4.3"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsc-alias":"^1.8.17","typescript":"^5.4.5","@types/cors":"^2.8.17","@types/node":"^20.14.0","ts-node-dev":"^2.0.0","@types/express":"^4.17.21","tsconfig-paths":"^4.2.0","@types/geoip-lite":"^1.4.4","@types/ua-parser-js":"^0.7.39"},"_npmOperationalInternal":{"tmp":"tmp/convert-service_1.0.1_1781292708690_0.6681119916208091","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@arthurzakharov/convert-service","version":"1.0.2","description":"Server-side Convert.com SDK proxy service","repository":{"url":"git+https://github.com/arthurzakharov/convert-service.git"},"author":{"name":"Artur Zakharov","email":"zakharov.arthur@icloud.com"},"publishConfig":{"access":"public"},"main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./client":{"types":"./dist/client.d.ts","default":"./dist/client.js"},"./contracts":{"types":"./dist/contracts.d.ts","default":"./dist/contracts.js"}},"scripts":{"build":"tsc && tsc-alias","start":"node dist/index.js","dev":"ts-node-dev -r tsconfig-paths/register --respawn --transpile-only src/index.ts","typecheck":"tsc --noEmit"},"dependencies":{"@convertcom/js-sdk":"^4.4.3","cors":"^2.8.5","dotenv":"^16.4.5","express":"^4.19.2","geoip-lite":"^2.0.2","ua-parser-js":"^2.0.10","zod":"^4.4.3"},"devDependencies":{"@types/cors":"^2.8.17","@types/express":"^4.17.21","@types/geoip-lite":"^1.4.4","@types/node":"^20.14.0","@types/ua-parser-js":"^0.7.39","ts-node-dev":"^2.0.0","tsc-alias":"^1.8.17","tsconfig-paths":"^4.2.0","typescript":"^5.4.5"},"gitHead":"8fd7b78a24b215d1a52f47d045f755bf172a8604","_id":"@arthurzakharov/convert-service@1.0.2","bugs":{"url":"https://github.com/arthurzakharov/convert-service/issues"},"homepage":"https://github.com/arthurzakharov/convert-service#readme","_nodeVersion":"24.11.1","_npmVersion":"11.6.2","dist":{"integrity":"sha512-1tCtoHtszik9hEfzS/oC65g+Ae0akKglkWr5YQfJn7MVYav6jP/Wl5XtBI7Abn9YHY7g5SxGUWml4jRUQn59lw==","shasum":"efe8853e6141e8a2689db02da2dab2a509a47502","tarball":"https://registry.npmjs.org/@arthurzakharov/convert-service/-/convert-service-1.0.2.tgz","fileCount":58,"unpackedSize":137839,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCOTWCBX2m6ZVOJOUM3ZR4jZyEUZoMl+gWATO3o9A8P8AIhAKT1wlBSlBVRVnnXle6kXOtwwo2R7JLJo3EK0Ef+t79p"}]},"_npmUser":{"name":"arthurzakharov","email":"zakharov.arthur@icloud.com"},"directories":{},"maintainers":[{"name":"arthurzakharov","email":"zakharov.arthur@icloud.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/convert-service_1.0.2_1781300050899_0.7975214614739512"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-12T18:13:46.282Z","modified":"2026-06-12T21:34:11.138Z","1.0.0":"2026-06-12T18:13:46.543Z","1.0.1":"2026-06-12T19:31:48.826Z","1.0.2":"2026-06-12T21:34:11.040Z"},"bugs":{"url":"https://github.com/arthurzakharov/convert-service/issues"},"author":{"name":"Artur Zakharov","email":"zakharov.arthur@icloud.com"},"homepage":"https://github.com/arthurzakharov/convert-service#readme","repository":{"url":"git+https://github.com/arthurzakharov/convert-service.git"},"description":"Server-side Convert.com SDK proxy service","maintainers":[{"name":"arthurzakharov","email":"zakharov.arthur@icloud.com"}],"readme":"# convert-service\n\nServer-side proxy for the [Convert.com](https://www.convert.com/) FullStack SDK. Exposes a small HTTP API so frontend apps can bucket visitors, read FullStack configuration, run feature flags, and track conversions without embedding SDK keys in the browser.\n\nThe bucketing endpoints follow Convert's documented FullStack flow for [experiences and variations](https://docs.developers.convert.com/docs/experiences-and-variations): create a visitor context, evaluate targeting/location rules, then return the selected variation or feature state.\n\n## Endpoints\n\n| Method | Path | Description |\n|--------|------|-------------|\n| `GET` | `/health` | Returns service version and commit hash |\n| `POST` | `/bucket` | Run all active experiences for a visitor |\n| `GET` | `/experiences` | List configured experiences for a project |\n| `GET` | `/experiences/:experienceKey` | Read one experience by key |\n| `GET` | `/experiences/by-id/:experienceId` | Read one experience by ID |\n| `GET` | `/experiences/:experienceKey/variations/:variationKey` | Read one variation by experience/variation key |\n| `GET` | `/experiences/by-id/:experienceId/variations/:variationId` | Read one variation by experience/variation ID |\n| `POST` | `/experiences/run` | Run one experience by key for a visitor |\n| `POST` | `/experiences/run-by-id` | Run one experience by ID for a visitor |\n| `GET` | `/features` | List configured FullStack features for a project |\n| `GET` | `/features/:featureKey` | Read one feature by key |\n| `GET` | `/features/by-id/:featureId` | Read one feature by ID |\n| `POST` | `/features/run-all` | Run all feature flags for a visitor |\n| `POST` | `/features/run` | Run one feature flag by key for a visitor |\n| `POST` | `/features/run-by-id` | Run one feature flag by ID for a visitor |\n| `POST` | `/track` | Track a conversion goal |\n\n### `GET /health`\n\n```json\n{\n  \"status\": \"ok\",\n  \"version\": \"1.0.0\",\n  \"commit\": \"a3f9c12\",\n  \"timestamp\": \"2026-06-11T17:00:00.000Z\"\n}\n```\n\n### `POST /bucket`\n\n```json\n// Request\n{\n  \"projectKey\": \"passexperten\",\n  \"visitorId\": \"user-unique-id\",\n  \"visitorProperties\": { \"country\": \"DE\" }\n}\n\n// Response\n{\n  \"visitorId\": \"user-unique-id\",\n  \"variations\": [\n    {\n      \"experienceKey\": \"exp-key\",\n      \"experienceName\": \"My Experiment\",\n      \"variationKey\": \"variation-1\",\n      \"variationName\": \"Variation 1\",\n      \"changes\": []\n    }\n  ]\n}\n```\n\n### Shared bucketing fields\n\n`POST /bucket`, `/experiences/run`, `/experiences/run-by-id`, `/features/run-all`, `/features/run`, and `/features/run-by-id` accept these Convert bucketing fields:\n\n| Field | Required | Description |\n|---|---|---|\n| `projectKey` | Yes | Supported project key, for example `passexperten` |\n| `visitorId` | Yes | Stable visitor identifier |\n| `visitorProperties` | No | Audience/segment targeting properties |\n| `locationProperties` | No | Location rule matching properties |\n| `pageUrl` | No | Page URL used while deriving default segments |\n| `campaign` | No | Campaign value used while deriving default segments |\n| `updateVisitorProperties` | No | Whether to update in-memory visitor properties |\n| `forceVariationId` | No | Force a specific variation ID when running experiences |\n| `enableTracking` | No | Whether Convert should track the bucketing event immediately |\n| `environment` | No | Override Convert environment for this decision |\n| `typeCasting` | No | Feature variable type casting flag |\n| `experienceKeys` | No | Limit feature evaluation to specific experience keys |\n\n### `GET /experiences`\n\n```http\nGET /experiences?projectKey=passexperten\n```\n\n```json\n{\n  \"experiences\": [\n    {\n      \"id\": \"100001\",\n      \"key\": \"headline-test\",\n      \"name\": \"Headline Test\",\n      \"status\": \"active\",\n      \"variations\": []\n    }\n  ]\n}\n```\n\n### `GET /experiences/:experienceKey`\n\n```http\nGET /experiences/headline-test?projectKey=passexperten\n```\n\n```json\n{\n  \"experience\": {\n    \"id\": \"100001\",\n    \"key\": \"headline-test\",\n    \"name\": \"Headline Test\",\n    \"variations\": []\n  }\n}\n```\n\nThe ID-based equivalent is:\n\n```http\nGET /experiences/by-id/100001?projectKey=passexperten\n```\n\n### `GET /experiences/:experienceKey/variations/:variationKey`\n\n```http\nGET /experiences/headline-test/variations/variation-a?projectKey=passexperten\n```\n\n```json\n{\n  \"variation\": {\n    \"id\": \"200001\",\n    \"key\": \"variation-a\",\n    \"name\": \"Variation A\",\n    \"changes\": []\n  }\n}\n```\n\nThe ID-based equivalent is:\n\n```http\nGET /experiences/by-id/100001/variations/200001?projectKey=passexperten\n```\n\n### `POST /experiences/run`\n\n```json\n// Request\n{\n  \"projectKey\": \"passexperten\",\n  \"visitorId\": \"user-unique-id\",\n  \"experienceKey\": \"headline-test\",\n  \"visitorProperties\": { \"country\": \"DE\" },\n  \"locationProperties\": { \"url\": \"https://www.passexperten.de/\" }\n}\n\n// Response\n{\n  \"visitorId\": \"user-unique-id\",\n  \"variation\": {\n    \"id\": \"200001\",\n    \"experienceId\": \"100001\",\n    \"experienceKey\": \"headline-test\",\n    \"experienceName\": \"Headline Test\",\n    \"variationKey\": \"variation-a\",\n    \"variationName\": \"Variation A\",\n    \"status\": \"running\",\n    \"bucketingAllocation\": 5000,\n    \"changes\": []\n  }\n}\n```\n\nThe ID-based request uses `experienceId`:\n\n```json\n{\n  \"projectKey\": \"passexperten\",\n  \"visitorId\": \"user-unique-id\",\n  \"experienceId\": \"100001\"\n}\n```\n\n### `GET /features`\n\n```http\nGET /features?projectKey=passexperten\n```\n\n```json\n{\n  \"features\": [\n    {\n      \"id\": \"300001\",\n      \"key\": \"checkout-flow\",\n      \"name\": \"Checkout Flow\",\n      \"variables\": [\n        { \"key\": \"enabled\", \"type\": \"boolean\" }\n      ]\n    }\n  ]\n}\n```\n\n### `POST /features/run-all`\n\n```json\n// Request\n{\n  \"projectKey\": \"passexperten\",\n  \"visitorId\": \"user-unique-id\",\n  \"visitorProperties\": { \"country\": \"DE\" }\n}\n\n// Response\n{\n  \"visitorId\": \"user-unique-id\",\n  \"features\": [\n    {\n      \"experienceId\": \"100001\",\n      \"experienceKey\": \"headline-test\",\n      \"experienceName\": \"Headline Test\",\n      \"id\": \"300001\",\n      \"key\": \"checkout-flow\",\n      \"name\": \"Checkout Flow\",\n      \"status\": \"enabled\",\n      \"variables\": { \"enabled\": true }\n    }\n  ]\n}\n```\n\n### `POST /features/run`\n\n```json\n// Request\n{\n  \"projectKey\": \"passexperten\",\n  \"visitorId\": \"user-unique-id\",\n  \"featureKey\": \"checkout-flow\",\n  \"experienceKeys\": [\"headline-test\"]\n}\n\n// Response\n{\n  \"visitorId\": \"user-unique-id\",\n  \"feature\": {\n    \"experienceId\": \"100001\",\n    \"experienceKey\": \"headline-test\",\n    \"id\": \"300001\",\n    \"key\": \"checkout-flow\",\n    \"status\": \"enabled\",\n    \"variables\": { \"enabled\": true }\n  },\n  \"features\": [\n    {\n      \"experienceId\": \"100001\",\n      \"experienceKey\": \"headline-test\",\n      \"id\": \"300001\",\n      \"key\": \"checkout-flow\",\n      \"status\": \"enabled\",\n      \"variables\": { \"enabled\": true }\n    }\n  ]\n}\n```\n\nThe ID-based request uses `featureId`:\n\n```json\n{\n  \"projectKey\": \"passexperten\",\n  \"visitorId\": \"user-unique-id\",\n  \"featureId\": \"300001\"\n}\n```\n\n### `POST /track`\n\n```json\n// Request\n{\n  \"projectKey\": \"passexperten\",\n  \"visitorId\": \"user-unique-id\",\n  \"goalKey\": \"purchase\",\n  \"attributes\": {\n    \"conversionData\": [\n      { \"key\": \"amount\", \"value\": 49.99 }\n    ]\n  }\n}\n\n// Response\n{ \"success\": true }\n```\n\n## Supported projects\n\n| `projectKey` | Site |\n|---|---|\n| `passexperten` | passexperten.de |\n| `bussgeldcheck` | bussgeldcheck.de |\n\n## Local development\n\n```bash\ncp .env.example .env\n# Fill in real SDK keys from Convert dashboard → Project Settings → SDK Keys\n\nnpm install\nnpm run dev        # ts-node-dev with hot reload on port 3100\n```\n\n## Frontend npm helper\n\nThis package also exposes a browser-safe helper for frontend applications. Import it from the `client` subpath so the frontend bundle does not import the Express service entrypoint:\n\n```ts\nimport { createConvertServiceClient } from 'convert-service/client';\n\nconst convert = createConvertServiceClient({\n  baseUrl: 'https://convert-service.example.com',\n  projectKey: 'passexperten',\n});\n\nconst { variation } = await convert.runExperience({\n  experienceKey: 'headline-test',\n  visitorProperties: { country: 'DE' },\n  locationProperties: { url: window.location.href },\n});\n\nif (variation?.variationKey === 'variation-a') {\n  // render variation-specific frontend behavior\n}\n```\n\nThe helper manages a stable visitor ID in a first-party cookie named `convert_visitor_id` by default. You can override or seed it when needed:\n\n```ts\nconst convert = createConvertServiceClient({\n  baseUrl: 'https://convert-service.example.com',\n  projectKey: 'bussgeldcheck',\n  visitorCookieName: 'bc_convert_vid',\n  visitorCookieMaxAgeDays: 180,\n  defaultVisitorProperties: { app: 'bussgeldcheck-web' },\n});\n\nconvert.setVisitorId(currentUser.id);\n```\n\nAvailable helper methods:\n\n| Method | Service endpoint |\n|---|---|\n| `health()` | `GET /health` |\n| `bucket()` | `POST /bucket` |\n| `listExperiences()` | `GET /experiences` |\n| `getExperienceByKey()` | `GET /experiences/:experienceKey` |\n| `getExperienceById()` | `GET /experiences/by-id/:experienceId` |\n| `getVariationByKey()` | `GET /experiences/:experienceKey/variations/:variationKey` |\n| `getVariationById()` | `GET /experiences/by-id/:experienceId/variations/:variationId` |\n| `runExperience()` | `POST /experiences/run` |\n| `runExperienceById()` | `POST /experiences/run-by-id` |\n| `listFeatures()` | `GET /features` |\n| `getFeatureByKey()` | `GET /features/:featureKey` |\n| `getFeatureById()` | `GET /features/by-id/:featureId` |\n| `runFeatures()` | `POST /features/run-all` |\n| `runFeature()` | `POST /features/run` |\n| `runFeatureById()` | `POST /features/run-by-id` |\n| `trackConversion()` | `POST /track` |\n\nEndpoint contracts are exported from `convert-service/contracts`:\n\n```ts\nimport type {\n  ConvertApiEndpoints,\n  EndpointRequest,\n  EndpointResponse,\n  RunFeatureResponse,\n} from 'convert-service/contracts';\n\ntype RunFeatureRequest = EndpointRequest<'POST /features/run'>;\ntype BucketResponse = EndpointResponse<'POST /bucket'>;\ntype AllEndpoints = keyof ConvertApiEndpoints;\n```\n\nUse these types in frontend wrappers, tests, or mocks when you need exact request/response compatibility with this service.\n\n## Environment variables\n\n| Variable | Required | Description |\n|---|---|---|\n| `CONVERT_SDK_KEY_PASSEXPERTEN` | Yes | SDK key for passexperten project |\n| `CONVERT_SDK_KEY_BUSSGELDCHECK` | Yes | SDK key for bussgeldcheck project |\n| `CONVERT_ENVIRONMENT` | Yes | `staging` or `live` |\n| `CONVERT_DATA_REFRESH_INTERVAL` | No | Config refresh interval in ms (default: `300000`) |\n| `CORS_ORIGINS` | No | Comma-separated allowed origins (default: `*`) |\n| `PORT` | No | Port to listen on (default: `3100`) |\n\n## Docker\n\n```bash\ndocker build -t convert-service .\ndocker run -p 3100:3100 --env-file .env convert-service\n```\n\n## Deployment\n\nThe service deploys to [Railway](https://railway.app) automatically via GitHub Actions.\n\n**Flow:**\n- Push to any branch / open a PR → CI runs (typecheck + build)\n- Merge to `main` → auto-deploys to Railway\n\n**One-time setup:**\n1. Create a Railway project and link it to this repo\n2. Set all env variables in the Railway dashboard\n3. Add `RAILWAY_TOKEN` to GitHub repo → Settings → Secrets and variables → Actions\n\n## Tech stack\n\n- Node 24 / TypeScript\n- Express\n- [@convertcom/js-sdk](https://www.npmjs.com/package/@convertcom/js-sdk)\n","readmeFilename":"README.md"}