API

The Ptally REST API

Price quotes in real time, manage projects and versions, run test suites and read your quote history. Requests and responses are JSON, and each user authenticates with their own key pair.

BASEhttps://ptally.ai/api
Authentication

Send your client_id_key and client_secret_key as headers on every request. Each user has their own pair, found in Settings; rotate it any time with PUT /users/:user_reference and refresh_keys: true.

curl https://ptally.ai/api/users/me \
  -H "client_id_key: $PTALLY_CLIENT_ID" \
  -H "client_secret_key: $PTALLY_CLIENT_SECRET"
Good to know
OpenAPI
GET https://ptally.ai/api/openapi returns an OpenAPI 3 document you can import into Postman or use to generate a client SDK. No credentials needed.
Dry runs
Add ?dry_run=true to a quote to price it without saving it to the history.
Pinning
Quotes use the latest approved or published version. Add ?version_reference=… to price a specific one.
Large models
Send version bodies gzipped with Content-Encoding: gzip, up to 100 MB.
Quickstart

Price the pet model

The pet insurance model priced for a 3-year-old neutered Persian in Greater London. The input is the flat set of fields the model defines; client_ref is optional and saved on the quote.

Request
curl -X POST "https://ptally.ai/api/quotes/$PROJECT_REFERENCE" \
  -H "client_id_key: $PTALLY_CLIENT_ID" \
  -H "client_secret_key: $PTALLY_CLIENT_SECRET" \
  -H "content-type: application/json" \
  -d '{"species":"cat","breed":"persian","pet_age":3,"region":"greater_london","vet_fee_limit":4000,"excess":100,"neutered":true,"client_ref":"POL-10234"}'
Response
{
  "quote_reference": "<uuid>",
  "project_reference": "<uuid>",
  "version_reference": "<uuid>",
  "version": 1,
  "valid": true,
  "status": "200",
  "result": 255.34,
  "client_ref": "POL-10234",
  "input": {
    "species": "cat",
    "breed": "persian",
    "pet_age": 3,
    "region": "greater_london",
    "vet_fee_limit": 4000,
    "excess": 100,
    "neutered": true
  },
  "output": {
    "valid": true,
    "result": 255.34,
    "risk_premium": 227.98,
    "ipt": 27.36,
    "monthly_premium": 21.28,
    "base_premium": 142,
    "refer": false
  },
  "error": null
}
What comes back
result
Annual premium in GBP including IPT: £255.34.
valid
true when no exclusion fired. A declined quote still returns a price but must not be bound.
quote_reference
The saved quote. Fetch it later with GET /quotes/:quote_reference. null on a dry run.
output
Everything the model exposes: the premium split, the monthly figure and refer, which is true when an underwriter must sign off before binding.
version
The version that priced it, so every quote is traceable to an exact model.
error
null unless the engine failed. The response also carries exec_time_ms and created_at.
How £255.34 was built
Base premiumcat · £4,000 limit£142.00
× Age factorage 3, band 2 to 41.00
× Breed factorPersian1.30
× Region factorGreater London1.30
× Excess factor£100 excess1.00
× Neutered factorneutered0.95
= Risk premiumrounded to the penny£227.98
+ IPT at 12%£27.36
= Annual premiumresult£255.34
Monthly÷ 12£21.28
QuotesPrice risks and read the quote history.
POST/quotes/:project_referenceRun a quote

Ask a project to price a risk. Send the input fields that match the project's form (get them from the editor or from the project's input schema) and the response carries the result, whether the quote is valid, and the engine's full output (refers, exclusions and step detail included). Add ?version_reference=… to pin a version of this project, any status (defaults to the latest approved or published version; 404 when there is none, when the pinned version doesn't exist or belongs to another project, and 400 when it is archived), ?dry_run=true to compute without saving the quote to the history (quote_reference comes back null), and ?source=… to tell the engine where the call came from (default api). A client_ref field in the body is saved on the quote. Works in read-only mode too.

Example request
{
  "base": 1200,
  "age": 34,
  "driving_experience": 10
}
Example response
{
  "quote_reference": "00000000-0000-0000-0000-000000000000",
  "project_reference": "00000000-0000-0000-0000-000000000000",
  "version_reference": "00000000-0000-0000-0000-000000000000",
  "version": 3,
  "valid": true,
  "status": "200",
  "result": 1247.5,
  "indicator": null,
  "client_ref": null,
  "input": {
    "base": 1200,
    "age": 34,
    "driving_experience": 10
  },
  "output": {
    "valid": true,
    "result": 1247.5,
    "premium": 1247.5
  },
  "exec_time_ms": 42,
  "error": null,
  "created_at": "2025-01-15T10:00:00.000Z"
}
GET/quotesList quotes

Browse the quotes your projects have generated, newest first (direction=asc for oldest first). Filter by project_reference, version_reference, since (an ISO date), or use search to text-match on indicators. Pass lite=true to drop the full input/output payloads and get just the summary fields. Paginated with limit (default 100) and offset.

Example response
[
  {
    "quote_reference": "00000000-0000-0000-0000-000000000000",
    "project_reference": "00000000-0000-0000-0000-000000000000",
    "version_reference": "00000000-0000-0000-0000-000000000000",
    "version": 3,
    "valid": true,
    "status": "200",
    "result": 1247.5,
    "indicator": "quote_indicator",
    "client_ref": "ext-12345",
    "exec_time_ms": 42,
    "error": null,
    "created_at": "2025-01-15T10:00:00.000Z"
  }
]
GET/quotes/analyticsQuote analytics

Time-series analytics for the quote volume, average result, validity rate, and errors. Pick a period: hour (last 60 minutes, per minute), day (last 24 hours, per hour; the default), week or month (last 7 or 30 days, per day), or this_month (since the 1st, per day), and the response comes back with a bucket per interval, ready to plot. Narrow it with project_reference, version_reference or since. Also includes overall average and p50/p90 execution time, and average and p50/p95 result.

Example response
{
  "type": "quotes",
  "count": 128,
  "errors": 3,
  "exec_time_avg": 47,
  "exec_time_p50": 42,
  "exec_time_p90": 91,
  "result_avg": 1247.5,
  "result_p50": 1102.3,
  "result_p95": 2450.75,
  "data": [
    {
      "date": "2025-01-15T10:00:00.000Z",
      "count": 12,
      "exec_time_avg": 42,
      "result_avg": 1247.5,
      "validity": 100,
      "errors": 0
    }
  ]
}
GET/quotes/:quote_referenceGet a quote

Fetch a single quote by its reference, including the full input payload you sent and the full output the engine produced. Useful for support tickets and for stepping through a specific rating decision. Returns 404 if the quote doesn't exist.

Example response
{
  "quote_reference": "00000000-0000-0000-0000-000000000000",
  "project_reference": "00000000-0000-0000-0000-000000000000",
  "version_reference": "00000000-0000-0000-0000-000000000000",
  "version": 3,
  "valid": true,
  "status": "200",
  "result": 1247.5,
  "indicator": "quote_indicator",
  "client_ref": "ext-12345",
  "exec_time_ms": 42,
  "error": null,
  "input": {
    "base": 1200,
    "age": 34
  },
  "output": {
    "valid": true,
    "result": 1247.5,
    "premium": 1247.5
  },
  "created_at": "2025-01-15T10:00:00.000Z"
}
ProjectsYour pricing products.
GET/projectsList your projects

Get every pricing project in your organisation. Each entry carries the name, description and version_reference / version of the project's highest-numbered version (whatever its status), plus versions_count. Archived projects are hidden by default. Add ?include_disabled=true to include them. Paginated with limit (default 200) and offset.

Example response
[
  {
    "project_reference": "00000000-0000-0000-0000-000000000000",
    "reference": "00000000-0000-0000-0000-000000000000",
    "name": "Motor",
    "description": "Personal-lines auto policy",
    "disabled": false,
    "created_at": "2025-01-15T10:00:00.000Z",
    "updated_at": "2025-01-15T10:00:00.000Z",
    "version_reference": "00000000-0000-0000-0000-000000000000",
    "version": 3,
    "versions_count": 3
  }
]
GET/projects/:project_referenceGet one project

Fetch the details of a single project: its name, description, and a pointer to the latest live (approved or published) version, or to the latest version of any status when nothing is live yet. Returns 404 if the project doesn't exist.

Example response
{
  "project_reference": "00000000-0000-0000-0000-000000000000",
  "reference": "00000000-0000-0000-0000-000000000000",
  "name": "Motor",
  "description": "Personal-lines auto policy",
  "disabled": false,
  "created_at": "2025-01-15T10:00:00.000Z",
  "updated_at": "2025-01-15T10:00:00.000Z",
  "version_reference": "00000000-0000-0000-0000-000000000000",
  "version": 3,
  "status": "published"
}
PUT/projects/:project_referenceUpdate project details

Rename a project, tweak its description, or archive/unarchive it with disabled. The change is applied to every version of the project so the header always stays consistent. Other fields are ignored. Archiving or restoring is recorded in GET /actions. Returns the project in the same shape as GET /projects/:project_reference, or 404 if the project doesn't exist.

Example request
{
  "name": "Motor (renamed)",
  "description": "Personal-lines auto policy. 2025 rework",
  "disabled": false
}
Example response
{
  "project_reference": "00000000-0000-0000-0000-000000000000",
  "reference": "00000000-0000-0000-0000-000000000000",
  "name": "Motor (renamed)",
  "description": "Personal-lines auto policy. 2025 rework",
  "disabled": false,
  "created_at": "2025-01-15T10:00:00.000Z",
  "updated_at": "2025-01-15T10:00:00.000Z",
  "version_reference": "00000000-0000-0000-0000-000000000000",
  "version": 3,
  "status": "published"
}
VersionsModels and their draft → approved → published lifecycle.
GET/projects/versionsList versions

List all versions across your projects, highest version number first. Add project_reference to focus on one project, or version_reference to look up a specific version. Archived versions are hidden by default. Pass ?include_disabled=true to see them. Models aren't included; fetch a single version to get its model.

Example response
[
  {
    "version_reference": "00000000-0000-0000-0000-000000000000",
    "project_reference": "00000000-0000-0000-0000-000000000000",
    "version": 3,
    "name": "Motor",
    "description": "Personal-lines auto policy",
    "summary": "Bumped base rate 4%",
    "status": "published",
    "disabled": false,
    "created_at": "2025-01-15T10:00:00.000Z",
    "updated_at": "2025-01-15T10:00:00.000Z",
    "approved_at": "2025-01-15T10:00:00.000Z",
    "published_at": "2025-01-15T10:00:00.000Z"
  }
]
POST/projects/versionsCreate a new project or version

Start a new draft version. Pass project_reference to add a version to an existing project. It copies the model from the latest live version (or the latest version when nothing is live); a project_reference that doesn't exist returns 404 rather than starting a new project. Leave it out to create a brand-new project with a starter model ready to edit. Optionally include model, tests, or process to seed content instead. Returns the new version with its model, in the same shape as GET /projects/versions/:version_reference.

Example request
{
  "project_reference": "00000000-0000-0000-0000-000000000000",
  "name": "Motor",
  "description": "Personal-lines auto policy",
  "summary": "Bumped base rate 4%"
}
Example response
{
  "version_reference": "00000000-0000-0000-0000-000000000000",
  "project_reference": "00000000-0000-0000-0000-000000000000",
  "version": 4,
  "name": "Motor",
  "description": "Personal-lines auto policy",
  "summary": "Bumped base rate 4%",
  "status": "draft",
  "disabled": false,
  "model": {},
  "created_at": "2025-01-15T10:00:00.000Z",
  "updated_at": "2025-01-15T10:00:00.000Z"
}
GET/projects/versions/:version_referenceGet a version

Fetch a version and its full pricing model: inputs, calculation steps, output, and any embedded tests and process notes. This is the payload the editor loads when you open a version. Returns 404 if the version doesn't exist.

Example response
{
  "version_reference": "00000000-0000-0000-0000-000000000000",
  "project_reference": "00000000-0000-0000-0000-000000000000",
  "version": 3,
  "name": "Motor",
  "description": "Personal-lines auto policy",
  "summary": "Bumped base rate 4%",
  "status": "published",
  "disabled": false,
  "model": {
    "input": {
      "format": {
        "base": {
          "type": "decimal",
          "def": 1000,
          "label": "Base"
        }
      }
    },
    "steps": [
      {
        "id": "s1",
        "step": "calculation",
        "key": "result",
        "name": "Result",
        "def": 0,
        "formula": "{{base}} * 1.04"
      }
    ],
    "output": {
      "format": {},
      "result": {
        "key": "result",
        "formula": "{{result}}",
        "type": "decimal",
        "def": 0
      },
      "valid": {
        "key": "valid",
        "exp": "true",
        "type": "boolean",
        "def": false
      }
    }
  },
  "created_at": "2025-01-15T10:00:00.000Z",
  "updated_at": "2025-01-15T10:00:00.000Z"
}
PUT/projects/versions/:version_referenceSave changes to a version

Save edits to a version, or move it through its lifecycle. model can only be changed on a draft; summary can be changed at any time. status moves a draft to approved, an approved version to published (or back to draft), a published version to unpublished, and an unpublished one back to published; any other move returns 400. Every status change is recorded in GET /actions. Project name and description are changed with PUT /projects/:project_reference. Returns the updated version with its model. Large models can be sent gzipped (Content-Encoding: gzip); the server accepts bodies up to 100 MB.

Example request
{
  "status": "published",
  "summary": "Bumped base rate 4%"
}
Example response
{
  "version_reference": "00000000-0000-0000-0000-000000000000",
  "project_reference": "00000000-0000-0000-0000-000000000000",
  "version": 3,
  "status": "published",
  "summary": "Bumped base rate 4%",
  "approved_at": "2025-01-15T10:00:00.000Z",
  "published_at": "2025-01-15T10:00:00.000Z",
  "model": {},
  "updated_at": "2025-01-15T10:00:00.000Z"
}
DELETE/projects/versions/:version_referenceArchive a version

Archive a draft version so it no longer appears in the default lists. Only drafts can be archived. Any other status returns 400. Nothing is permanently deleted. Pass ?include_disabled=true on the list endpoints to still see it. The archive is recorded in GET /actions. Returns { "deleted": 1 }, or { "deleted": 0 } when the version doesn't exist.

Example response
{
  "deleted": 1
}
Tests & process notesThe test suite and notes attached to a version.
GET/process/:version_referenceGet a version's process notes

Fetch the freeform process notes attached to a version: the notes shown under the "Process" tab of the version editor. Returns an empty object {} when nothing has been written (or the version doesn't exist).

Example response
{
  "summary": {
    "content": "<p>Process notes rendered as HTML.</p>"
  }
}
POST/process/:version_referenceSave process notes

Replace the process notes on a draft version. Send the full block you want stored. This overwrites what was there, and the response echoes it back. Only draft versions can be edited; any other status returns 400, and an unknown version returns 404.

Example request
{
  "summary": {
    "content": "<p>Approved by pricing team on 2025-01-15.</p>"
  }
}
Example response
{
  "summary": {
    "content": "<p>Approved by pricing team on 2025-01-15.</p>"
  }
}
GET/tests/:version_referenceGet a version's tests

Fetch the test cases attached to a version. Each test is a named input/expected-output pair used to check the model is still pricing the way it should. tests is an empty array when no tests have been added yet (or the version doesn't exist).

Example response
{
  "tests": [
    {
      "id": "test_1",
      "name": "Young London driver",
      "key": "test_1",
      "input": {
        "base": 1200,
        "age": 22
      },
      "output": {
        "result": 1560,
        "valid": true
      }
    }
  ]
}
POST/tests/:version_referenceSave tests

Replace the test suite on a draft version. Send { "tests": [...] } (or a bare array) with the full list you want stored. Old tests are overwritten. Only draft versions can be edited; any other status returns 400, and an unknown version returns 404.

Example request
{
  "tests": [
    {
      "id": "test_1",
      "name": "Young London driver",
      "key": "test_1",
      "input": {
        "base": 1200,
        "age": 22
      },
      "output": {
        "result": 1560,
        "valid": true
      }
    }
  ]
}
Example response
{
  "tests": [
    {
      "id": "test_1",
      "name": "Young London driver",
      "key": "test_1",
      "input": {
        "base": 1200,
        "age": 22
      },
      "output": {
        "result": 1560,
        "valid": true
      }
    }
  ]
}
HistoryWho changed what, and when.
GET/actionsVersion history

See what happened to each version, when, and by whom. Useful for audit and for showing an activity feed. Returns a list of events, newest first. type is one of create, approve, publish, unpublish, revert_to_draft (approved back to draft), archive (a draft version archived), or archive_project / unarchive_project (the whole project archived or restored with PUT /projects/:project_reference; these carry a null version_reference). Every event is kept, so a version published, unpublished and published again shows all three. Status changes carry the move in content (e.g. approved → published). user_reference is the user who did it and email their address (null if unknown or since removed). History from before this audit log existed is rebuilt from the version itself: its creation, and its latest approval and publication. Pass version_reference to scope to one version; limit caps how many events come back (default 100).

Example response
[
  {
    "action_reference": "00000000-0000-0000-0000-000000000000",
    "version_reference": "00000000-0000-0000-0000-000000000000",
    "project_reference": "00000000-0000-0000-0000-000000000000",
    "user_reference": "00000000-0000-0000-0000-000000000000",
    "email": "jane@example.com",
    "type": "approve",
    "content": "draft → approved",
    "created_at": "2025-01-15T10:00:00.000Z"
  }
]
Users & companyYour team, their roles and API keys.
GET/companyYour company details

Fetch your organisation's name and reference, for displaying the company profile. Any user can call it. The master keys (company_client_id_key / company_client_secret_key) are only included for an owner (the super_admin role); everyone else gets the row without them. They aren't API credentials either: this server's API only accepts a user's own client_id_key / client_secret_key.

Example response
{
  "company_reference": "00000000-0000-0000-0000-000000000000",
  "company_name": "Acme Insurance Ltd",
  "company_client_id_key": "00000000-0000-0000-0000-000000000000",
  "company_client_secret_key": "00000000-0000-0000-0000-000000000000",
  "created_at": "2025-01-15T10:00:00.000Z",
  "updated_at": "2025-01-15T10:00:00.000Z"
}
PUT/companyUpdate your company

Rename your organisation, or rotate the master keys if you think they've been leaked. Renaming needs an admin or owner. A plain user gets a 403. Only an owner (the super_admin role) can rotate keys. Anyone else gets a 403. Returns the updated company; as with GET /company, the master keys are only included for an owner.

Example request
{
  "company_name": "Acme Insurance Ltd",
  "refresh_master_keys": false
}
Example response
{
  "company_reference": "00000000-0000-0000-0000-000000000000",
  "company_name": "Acme Insurance Ltd",
  "company_client_id_key": "00000000-0000-0000-0000-000000000000",
  "company_client_secret_key": "00000000-0000-0000-0000-000000000000",
  "created_at": "2025-01-15T10:00:00.000Z",
  "updated_at": "2025-01-15T10:00:00.000Z"
}
GET/usersList users

List everyone in your organisation, oldest first, with their role. Any user can call it. Useful for a team-management page or for auditing who has access. Only your own row includes API credentials (client_id_key / client_secret_key); everyone else's comes back without them. role is super_admin (shown as Owner in the dashboard), admin or user.

Example response
[
  {
    "user_reference": "00000000-0000-0000-0000-000000000000",
    "email": "user@example.com",
    "role": "admin",
    "is_admin": true,
    "is_admin_primary": false,
    "is_enabled": true,
    "created_at": "2025-01-15T10:00:00.000Z"
  }
]
GET/users/meWho am I?

Get the user record for whoever is making the request, resolved from the credentials on the call. Handy for showing "logged in as…" state and for checking your own role.

Example response
{
  "user_reference": "00000000-0000-0000-0000-000000000000",
  "email": "user@example.com",
  "role": "admin",
  "is_admin": true,
  "is_admin_primary": false,
  "is_enabled": true,
  "client_id_key": "00000000-0000-0000-0000-000000000000",
  "client_secret_key": "00000000-0000-0000-0000-000000000000",
  "created_at": "2025-01-15T10:00:00.000Z"
}
POST/usersInvite a user

Add a new teammate to your organisation. Needs an admin or owner. A plain user gets a 403. email is required (400 if missing) and must be unique (409 if taken). The response includes their personal API credentials. Share them with the new user so they can sign in. Role defaults to user; pass role: "admin" (or is_admin: true) to grant admin access, or role: "super_admin" for another owner. Creating an admin or owner needs an owner (super_admin). An admin gets a 403. Any other role is a 400.

Example request
{
  "email": "new-user@example.com",
  "role": "user"
}
Example response
{
  "user_reference": "00000000-0000-0000-0000-000000000000",
  "email": "new-user@example.com",
  "role": "user",
  "is_admin": false,
  "is_admin_primary": false,
  "is_enabled": true,
  "client_id_key": "00000000-0000-0000-0000-000000000000",
  "client_secret_key": "00000000-0000-0000-0000-000000000000",
  "created_at": "2025-01-15T10:00:00.000Z"
}
PUT/users/:user_referenceUpdate a user

Make a teammate an admin (is_admin: true) or a plain user (is_admin: false), disable their access with is_enabled: false, or rotate their API keys with refresh_keys: true. Only those three fields are accepted. Anything else (role, email, keys…) is a 400 naming the field. is_admin only switches between user and admin: it never makes someone an owner (super_admin). Anyone can rotate their own keys; changing anyone else needs an admin or owner (403 otherwise). Nobody can change their own admin flag or disable themselves (403). An owner can't be changed by anyone, except to have their keys rotated by an owner (403 otherwise). A field sent with the value it already has is ignored. 404 for an unknown user. Returns the updated user, new keys included.

Example request
{
  "is_admin": true,
  "is_enabled": true
}
Example response
{
  "user_reference": "00000000-0000-0000-0000-000000000000",
  "email": "user@example.com",
  "role": "admin",
  "is_admin": true,
  "is_admin_primary": false,
  "is_enabled": true,
  "client_id_key": "00000000-0000-0000-0000-000000000000",
  "client_secret_key": "00000000-0000-0000-0000-000000000000",
  "created_at": "2025-01-15T10:00:00.000Z"
}
DELETE/users/:user_referenceRemove a user

Remove a teammate from your organisation. Needs an admin or owner. A plain user gets a 403. You can't remove yourself, and an owner (super_admin) can't be removed through the API. Both return 400. Returns { "deleted": 1 }, or { "deleted": 0 } when the user doesn't exist.

Example response
{
  "deleted": 1
}
ReferenceMachine-readable spec.
GET/openapiAPI reference

Get an OpenAPI 3.0 document describing every endpoint the server exposes, with parameters, request/response shapes, and worked examples. Handy for generating client SDKs or importing into Postman/Insomnia. Open to anyone, no credentials needed.

Example response
{
  "openapi": "3.0.0",
  "info": {
    "title": "Ptally",
    "version": "0.1.0"
  },
  "paths": {
    "/example": {
      "get": {
        "summary": "…"
      }
    }
  }
}