REST API
The Personizely REST API lets your own code, scripts and AI tools read your campaigns, widgets, goals and targets, pull A/B test results with statistics, and start or stop campaigns and widgets. It's a JSON API over HTTPS, authenticated with an API key or an OAuth 2.1 access token. This page covers what every request has in common; each endpoint has its own reference page, linked in the endpoint index.
Base URL
https://api.personizely.net/v1All endpoints are relative to this URL. Requests and responses use JSON. An interactive OpenAPI reference is available at https://api.personizely.net/docs, and the machine-readable spec at https://api.personizely.net/docs.json, which you can import into Postman or a client generator.
Authentication
Send a bearer token in the Authorization header of every request:
curl https://api.personizely.net/v1/websites \
-H "Authorization: Bearer ply_live_XXXXXXXXXXXXXXXX"The API accepts two kinds of token:
Token | Looks like | Best for | Access |
|---|---|---|---|
API key | Starts with ply_live_ | Your own scripts, internal dashboards, scheduled jobs, AI tools that accept a custom header | Read data, and start and stop campaigns and widgets, for every website in the account |
OAuth 2.1 access token | A JWT (starts with eyJ) | Apps that act on behalf of other Personizely users, and AI assistants such as Claude and ChatGPT | The scopes the user approved, for the account they chose |
Both kinds of token give access to one Personizely account and all its websites.
API keys
An account owner or admin creates API keys under Account → API. The key is shown once, when it's created; store it in a secret manager. Keys can have an expiry date and can be revoked at any time. See API keys and connected apps.
API keys can read all data and start and stop campaigns and widgets. Don't put them in browser code, mobile apps or public repositories. The storefront JavaScript API (window.ply) doesn't need one.
OAuth 2.1
Use OAuth when your app is used by Personizely customers other than you, so that each user signs in and approves access instead of pasting a key. Personizely runs a standard OAuth 2.1 authorization server:
| Value |
|---|---|
Discovery metadata | https://app.personizely.net/.well-known/oauth-authorization-server |
Authorization endpoint | https://app.personizely.net/oauth/authorize |
Token endpoint | https://app.personizely.net/oauth/token |
Dynamic client registration | https://app.personizely.net/oauth/register (RFC 7591) |
Grant types | authorization_code, refresh_token |
PKCE | Required, S256 only |
Client authentication | none (public clients), client_secret_basic, client_secret_post |
Scopes | read (view data), write (start and stop campaigns and widgets only) |
Access token lifetime | 1 hour |
Refresh token lifetime | 30 days |
Registering a client. Register your app by sending its metadata to the registration endpoint. There's no manual approval step.
curl -X POST https://app.personizely.net/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "Acme Reporting",
"redirect_uris": ["https://acme.example/oauth/callback"],
"scope": "read write",
"token_endpoint_auth_method": "client_secret_basic"
}'Field | Rules | Default |
|---|---|---|
client_name | 1–255 characters; shown to users on the consent screen | Unknown App |
redirect_uris | Required, at least one. https URLs; http only for localhost, 127.0.0.1 or [::1]; custom schemes for native apps. | – |
grant_types | authorization_code, refresh_token | Both |
token_endpoint_auth_method | none creates a public client with no secret. client_secret_basic or client_secret_post creates a confidential client and returns a client_secret. | none |
scope | Space-separated: read, write | read |
The response contains your client_id, and client_secret for confidential clients. The secret is returned only once.
Authorizing. Send the user to the authorization endpoint with your client_id, redirect_uri, response_type=code, scope, state and a PKCE code_challenge (code_challenge_method=S256). The user signs in to Personizely, picks which account to connect if they belong to several, and approves. Only an owner or admin of that account can approve. You then exchange the code at the token endpoint for an access token and a refresh token.
Scopes. read covers every GET endpoint. write is needed to start and stop campaigns and widgets. If you don't request a scope, you get read.
Refreshing. Access tokens expire after an hour. Use the refresh token to get a new pair; refresh tokens last 30 days. Users can disconnect your app from Account → API, which revokes its tokens.
Choosing a website
A Personizely account can have Multiple websites, each with its own campaigns, widgets, goals and targets. Every endpoint except GET /websites works on one website, chosen with the websiteId query parameter:
curl "https://api.personizely.net/v1/campaigns?websiteId=4821&active=true" \
-H "Authorization: Bearer $PLY_API_KEY"- If the account has exactly one website, you can leave websiteId out.
- If it has more than one, websiteId is required. Without it, you get a 400 telling you to pass it.
- Get the IDs from GET /websites, which lists every website in the account with its id and domain, sorted by domain:
[
{ "id": 4821, "domain": "shop.example.com" },
{ "id": 5310, "domain": "wholesale.example.com" }
]Always send websiteId in integrations you share with others. An account with one website today may add a second one later.
Pagination
List endpoints use offset and limit and return a plain JSON array.
Endpoint | limit range | Default limit |
|---|---|---|
GET /campaigns, GET /widgets | 1–500 | 100 |
GET /goals, GET /targets | 1–1000 | 500 |
offset starts at 0. Responses don't include a total count: keep requesting the next page until you get fewer items than your limit. Campaigns are sorted newest first.
Results and filters
The results endpoints return the same numbers as the campaign and widget reports in the dashboard: reach, views, conversions, revenue and, for experiments, significance and improvement against control. See Statistics methodology for how they're calculated.
Narrow results with filters, passed as a query-string map: filters[field]=value, or filters[field][]=a&filters[field][]=b for lists.
Filter | Type | Notes |
|---|---|---|
dateFrom, dateTo | date | 2026-03-15 or 2026-03-15T10:30:00Z. Without them, results run from when the campaign or widget was created until now. |
device, browser, os | list | |
country, region, city | list | |
utmSource, utmMedium, utmCampaign, utmContent, utmTerm | string | |
referrer, firstVisitPage | string | |
visitorType | new or returning | |
goalValue, zScore | number | Results endpoints only |
goalTags | list | Shopify order tags; counts only orders carrying at least one of them |
Unknown filter names are rejected with a 400.
curl -G "https://api.personizely.net/v1/campaigns/3107/results" \
-H "Authorization: Bearer $PLY_API_KEY" \
--data-urlencode "websiteId=4821" \
--data-urlencode "compareMetric=valuePerVisitor" \
--data-urlencode "filters[dateFrom]=2026-09-01" \
--data-urlencode "filters[device][]=mobile"Errors
Errors use standard HTTP status codes. The JSON body has the status code and a human-readable message. Authentication errors (401) return only the message.
{ "code": 404, "message": "<what went wrong>" }Status | Meaning | Common causes |
|---|---|---|
400 | Bad request | websiteId missing on a multi-website account; a parameter out of range or not in its allowed list; an unknown filter; conversionRate or completionRate used on a widget experiment with a control group |
401 | Not authenticated | Missing, invalid, revoked or expired API key or access token |
403 | Not allowed | The token only has the read scope and you called a start or stop endpoint |
404 | Not found | The website, campaign, widget, goal or target doesn't exist or belongs to another account |
5xx | Server error | Retry with backoff; contact support if it persists |
{ "message": "API key has expired" }The 401 messages are Invalid API key, API key has been revoked, API key has expired, Invalid OAuth access token, Access token not found, Access token has been revoked and Access token has expired. With OAuth, an expired access token means it's time to use your refresh token.
Endpoint index
Method | Path | What it does | Scope |
|---|---|---|---|
GET | /websites | read | |
GET | /campaigns | List campaigns, filtered by active and type | read |
GET | /campaigns/{id} | Get a campaign with its variations | read |
POST | /campaigns/{id}/start | write | |
POST | /campaigns/{id}/stop | write | |
GET | /campaigns/{id}/results | Get campaign results, by goal, with optional breakdown (device, country, channel, referrer) and filters | read |
GET | /widgets | List widgets, filtered by active and type (popup, callout, bar, embedded) | read |
GET | /widgets/{id} | Get a widget with its variations and experiments | read |
POST | /widgets/{id}/start | write | |
POST | /widgets/{id}/stop | write | |
GET | /widgets/{id}/results | read | |
GET | /widgets/{id}/experiments/{experimentId}/results | read | |
GET | /goals | read | |
GET | /goals/{id} | read | |
GET | /targets | List targets (names and dates, not rules) | read |
GET | /targets/{id} | read |
The API can't create or edit campaigns, widgets, goals or targets. Use the dashboard for that.
Behavior details and limitations
- Start and stop have the same effect as in the dashboard. The change goes live on your site and is recorded as an update to the campaign or widget. Starting something that's already running, or stopping something already stopped, returns it unchanged.
- Results default to the primary goal and primary metric. GET /campaigns/{id}/results uses the campaign's primary goal unless you pass goal, and its primary metric unless you pass compareMetric (goalConversionRate, valuePerVisitor, profitPerVisitor or aov).
- Significance is only calculated without a breakdown. When you pass dimension, you get per-segment numbers but no significance, as in the dashboard. Personalizations return raw numbers without statistics.
- Widget experiment statistics use fixed settings: frequentist, 95% significance, 80% power and a 20% minimum detectable effect.
- Conversion rate counts visitors. Each result row includes both goalsReached (total goal completions) and uniqueGoalsReached (visitors who completed the goal). Conversion rate is based on visitors.
- Results refresh every few minutes. A conversion that just happened may take a few minutes to appear. There's no need to poll more often than that.
- Targets don't expose their rules. The target endpoints return ID, name and dates, so you can map targetId values to names.
- Results can't be filtered by product. Order tags (goalTags) are supported; product filters available in the dashboard aren't.
Examples
Daily Slack digest. A scheduled job calls GET /campaigns?active=true, then GET /campaigns/{id}/results for each one, and posts each variation's improvement and significance to a channel.
Stop a test from your release pipeline. Before deploying a redesign that would invalidate a running test, call POST /campaigns/{id}/stop with a key stored in your CI secrets.
Ask an AI assistant about your tests. Connect Claude, ChatGPT or another MCP client to the MCP server, which uses this API with OAuth.
Related
- MCP server