SaveCall logo SaveCall API docs
Public API docs

SaveCall API v1

SaveCall exposes a real staging-grade API for resolving phone numbers with page context, recommending capture mode by country, collecting resolution feedback, and handling billing state.

These docs intentionally separate validated current behavior from draft policy. They are suitable for cautious pre-launch public use.

API Overview

Validated now

The API is already a separate runnable server module. Repo truth confirms live support for /health, /v1/resolve, /v1/mode-recommendation, /v1/feedback/resolution, and billing endpoints including checkout, portal, state, and webhook handling.

Main inputs:

  • a raw phone number
  • user/search country and language
  • capture mode and source type
  • origin/page context from the source page
  • clientRef for tenant-aware billing/auth flows

Important non-promises

  • No public production SLA is promised here.
  • No guarantee of perfect identity resolution for every number or every language.
  • No promise that pricing, packaging, or plan semantics are final.
  • No claim that backend persistence and operations are fully production-hardened yet.
Good fit today: extension or backend integrators who want context-aware number resolution, internal tooling, staged partner integrations, and billing-backed tenant testing.

Concrete use cases:

  • resolve a tel: link into a likely contact name plus alternatives
  • choose the recommended capture mode for a launch country
  • submit post-resolution feedback to improve product loops
  • start a Stripe checkout or billing portal flow tied to a SaveCall tenant

Quickstart

Authentication is header-based via X-SaveCall-Api-Key. When you supply a clientRef to /v1/resolve, the API key must be bound to that same client reference.

clientRef is the SaveCall tenant/customer identifier used across checkout, webhook state, billing state resolution, and resolve enforcement.

Minimal health check

curl -s https://your-savecall-host/health

Minimal resolve example

curl -s -X POST https://your-savecall-host/v1/resolve \
  -H "Content-Type: application/json" \
  -H "X-SaveCall-Api-Key: sca_staging_..." \
  -d '{
    "rawNumber": "+351239410000",
    "userCountry": "PT",
    "searchCountry": "PT",
    "searchLanguage": "pt",
    "captureMode": "EXTENSION",
    "sourceType": "TEL_LINK",
    "browser": "firefox",
    "clientRef": "staging-tenant-001",
    "origin": {
      "url": "https://www.uc.pt/contactos",
      "title": "Universidade de Coimbra — Contactos",
      "host": "www.uc.pt",
      "nearHeading": "Contactos",
      "pageExcerpt": "Universidade de Coimbra contactos telefónicos"
    },
    "preferences": {
      "confirmBeforeSaving": false,
      "allowNumberOnly": true,
      "preferEntityNameForTelLink": true
    }
  }'

Note: clientRef is enforced directly in the server code for /v1/resolve, but it is not yet part of the published contract DTO file. This docs page reflects the current live server behavior rather than an idealized contract-only view.

Example resolve response shape

{
  "resolutionId": "res_...",
  "status": "RESOLVED",
  "recommendedAction": "SAVE_CONTACT",
  "userMessage": "Resolved with strong page context",
  "allowNumberOnly": true,
  "normalizedNumber": "+351239410000",
  "confidence": {
    "score": 0.92,
    "level": "HIGH",
    "label": "High confidence"
  },
  "bestMatch": {
    "id": "match_...",
    "displayName": "Universidade de Coimbra",
    "phoneE164": "+351239410000",
    "sourceUrl": "https://www.uc.pt/contactos",
    "sourceLabel": "page context",
    "entityType": "institution",
    "reasonSummary": "Strong origin and page signals"
  },
  "alternatives": [],
  "contextProvenance": "origin+page",
  "debugTraceId": "trace_..."
}

Billing checkout example

curl -s -X POST https://your-savecall-host/billing/checkout \
  -H "Content-Type: application/json" \
  -d '{"plan":"TEAM","clientRef":"staging-tenant-001"}'
{
  "stub": false,
  "plan": "TEAM",
  "clientRef": "staging-tenant-001",
  "checkoutConfigured": true,
  "checkoutSessionId": "cs_...",
  "checkoutUrl": "https://checkout.stripe.com/..."
}

The plan field above reflects the request and current server-side billing identifiers. It should not be read as final public pricing or finalized commercial packaging.

JavaScript example

const response = await fetch("https://your-savecall-host/v1/resolve", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-SaveCall-Api-Key": "sca_staging_..."
  },
  body: JSON.stringify({
    rawNumber: "+351239410000",
    userCountry: "PT",
    searchCountry: "PT",
    searchLanguage: "pt",
    captureMode: "EXTENSION",
    sourceType: "TEL_LINK",
    clientRef: "staging-tenant-001"
  })
});
const data = await response.json();

Python example

import requests

response = requests.post(
    "https://your-savecall-host/v1/resolve",
    headers={
        "Content-Type": "application/json",
        "X-SaveCall-Api-Key": "sca_staging_...",
    },
    json={
        "rawNumber": "+351239410000",
        "userCountry": "PT",
        "searchCountry": "PT",
        "searchLanguage": "pt",
        "captureMode": "EXTENSION",
        "sourceType": "TEL_LINK",
        "clientRef": "staging-tenant-001",
    },
)
print(response.status_code, response.json())

API Reference

GET/health

No auth required.

Returns basic server health and version identity.

{
  "status": "ok",
  "service": "save-call-api",
  "version": "v1-file-backed-runtime"
}

Error cases: 405 for non-GET methods.

GET/v1/mode-recommendation?country=PT

No auth required.

Returns the recommended capture mode for a given country, plus benchmark metadata from current policy data.

ParameterTypeNotes
countryquery stringPreferred query parameter
userCountryquery stringAlso accepted by the server as fallback
{
  "countryCode": "PT",
  "recommendedMode": "EXTENSION",
  "reason": "Current benchmark policy favours EXTENSION for PT",
  "benchmarks": [],
  "defaultSearchCountry": "PT",
  "defaultSearchLanguage": "pt",
  "sourceLabel": "policy_json",
  "fallbackUsed": false
}

Error cases: 405 for non-GET methods.

POST/v1/resolve

Auth is conditional on clientRef presence and environment mode. This endpoint is quota-gated.

Core request fields from the contract:

  • rawNumber, userCountry, searchCountry, searchLanguage
  • captureMode: EXTENSION or CHROME
  • sourceType: TEL_LINK, SELECTION, CONTEXT_MENU, PAGE_DETECTION, MANUAL, UNKNOWN
  • origin page context fields including structural anchor fields when available
  • preferences for confirmation and number-only behavior

Current server truth also supports clientRef in the JSON body for auth, billing, and quota enforcement.

Example success statuses include RESOLVED, AMBIGUOUS, NUMBER_ONLY, and NOT_FOUND.

Representative error cases:

  • 400 missing_client_ref in strict mode
  • 401 missing_api_key
  • 401 invalid_api_key
  • 403 client_ref_mismatch
  • 402 inactive_subscription
  • 429 plan_quota_exceeded
  • 400 invalid request body

POST/v1/feedback/resolution

No API-key enforcement is implemented here in the current server file.

Request body fields:

  • resolutionId
  • action: ACCEPTED_BEST_MATCH, ACCEPTED_ALTERNATIVE, SAVED_NUMBER_ONLY, REJECTED, EDITED_NAME_BEFORE_SAVE, ABANDONED
  • savedAsName, selectedAlternativeIndex, editedName

Response status is 202 Accepted.

{
  "accepted": true,
  "message": "Feedback accepted"
}

Error cases: 400 invalid body, 405 wrong method.

POST/billing/checkout

No API key required in current server code.

Request body:

{
  "plan": "TEAM",
  "clientRef": "staging-tenant-001"
}

The current billing handlers accept plan identifiers DEVELOPER, TEAM, and GROWTH.

Validated live behavior incorporated here: checkout creates a real Stripe Checkout Session when configured.

Those identifiers are operational billing inputs, not a finalized public pricing table.

Error cases:

  • 400 unknown plan or invalid JSON
  • 503 missing Stripe configuration
  • 502 Stripe API error

POST/billing/portal

No API key required in current server code.

Primary request body:

{
  "clientRef": "staging-tenant-001"
}

The handler prefers resolving the Stripe customer through clientRef. A direct customerId body field exists as an escape hatch. A provisional global fallback also exists and is intentionally documented as provisional, not ideal.

{
  "stub": false,
  "portalConfigured": true,
  "portalReturnUrl": "https://example.com/account",
  "clientRef": "staging-tenant-001",
  "resolvedVia": "clientRef",
  "customerId": "cus_...",
  "portalSessionId": "bps_...",
  "portalUrl": "https://billing.stripe.com/..."
}

Error cases:

  • 503 portal configuration missing
  • 404 no customer resolved yet
  • 502 Stripe API error

GET/billing/state?clientRef=...

No API key required in current server code.

Returns the effective billing state derived from the local webhook event store. It does not call Stripe live.

{
  "stub": false,
  "clientRef": "staging-tenant-001",
  "customerId": "cus_UEfRC62pbeT6Fw",
  "subscriptionId": "sub_1TGC6PFWBKL8DaOfumw75oBJ",
  "plan": null,
  "priceId": null,
  "status": "active",
  "isActive": true,
  "resolvedFrom": "webhook_event_store:checkout.session.completed",
  "resolvedAtMs": 1774762899300,
  "sourceEventId": "evt_1TGC6QFWBKL8DaOfDZddiblc",
  "sourceEventReceivedAt": 1774762899300
}

The example above is grounded in the file-backed billing event store present in the repo runtime directory.

Important: isActive is currently the most reliable public interpretation here. plan, priceId, and related mapping details may legitimately be null or provisional depending on webhook data.

Error cases: 400 missing clientRef, 405 wrong method.

POST/stripe/webhook

This is an internal billing integration endpoint, not a normal public consumer endpoint.

It validates the Stripe-Signature header and processes:

  • checkout.session.completed
  • customer.subscription.updated
  • customer.subscription.deleted

Error cases: 400 missing/invalid signature, 503 webhook not configured.

Auth & Quotas

X-SaveCall-Api-Key

The server reads the API key from the X-SaveCall-Api-Key header. API keys are bound to specific clientRef values in the API runtime store.

clientRef binding

When a clientRef is present on /v1/resolve, the request flows through this enforcement chain:

  1. API key must be present
  2. API key must be known
  3. API key must match the same clientRef
  4. billing must be active for that clientRef
  5. quota evaluation is then applied using the server's current billing/usage interpretation

TRANSITIONAL vs STRICT

ModeEnv varBehavior when clientRef is missing
TRANSITIONALSAVE_CALL_REQUIRE_CLIENT_REF=false or unsetRequest is allowed and response includes X-SaveCall-Warning: missing-client-ref
STRICTSAVE_CALL_REQUIRE_CLIENT_REF=trueRequest is rejected with 400 missing_client_ref

Common auth errors

{"error":true,"code":"missing_api_key","message":"Header X-SaveCall-Api-Key is required when clientRef is supplied"}
{"error":true,"code":"invalid_api_key","message":"The provided API key is not recognised"}
{"error":true,"code":"client_ref_mismatch","clientRef":"staging-tenant-001","message":"The API key is not authorised for clientRef \"staging-tenant-001\""}

Quota headers

The server emits these headers once quota evaluation is reached on /v1/resolve:

  • X-SaveCall-Plan
  • X-SaveCall-Quota-Limit
  • X-SaveCall-Quota-Used
  • X-SaveCall-Quota-Remaining
X-SaveCall-Plan: unknown
X-SaveCall-Quota-Limit: 50
X-SaveCall-Quota-Used: 37
X-SaveCall-Quota-Remaining: 13
Commercial wording is still provisional. The current server code and staging docs contain operational quota defaults and expose quota headers, but the meaning of X-SaveCall-Plan should be read cautiously while plan/price mapping remains operationally evolving. Treat these headers as useful runtime signals, not as a finalized public pricing contract.

Errors

Current error responses are JSON objects. Two shapes exist today:

  • generic ack-style errors from helper paths, usually {"accepted":false,"message":"..."}
  • structured enforcement errors from /v1/resolve, with error, code, and message
{
  "error": true,
  "code": "inactive_subscription",
  "clientRef": "staging-tenant-001",
  "message": "No active subscription for clientRef \"staging-tenant-001\""
}

Common codes:

  • missing_client_ref
  • missing_api_key
  • invalid_api_key
  • client_ref_mismatch
  • inactive_subscription
  • plan_quota_exceeded

What integrators should do:

  • treat 400 as request-shape or strict-mode input issues
  • treat 401/403 as auth or tenant-binding failures
  • treat 402 as billing-state remediation needed
  • treat 429 as quota exhaustion and surface quota headers if present
  • treat quota and plan identifiers as operational signals rather than final commercial labels
  • log debugTraceId from successful resolve responses when debugging outcome quality

Versioning & Changelog

The current public surface is documented as API v1. Endpoint names are already versioned under /v1/ where appropriate.

Breaking changes should be introduced conservatively. Because the API is still in a staging-to-prelaunch phase, anything not yet marked stable in these docs should be treated as subject to tightening rather than frozen forever.

OpenAPI draft status

The linked openapi.yaml is a conservative draft to help integrators explore the surface. It is not yet the sole canonical contract source, and current live server behavior may still be described more precisely in these docs where operational truth exceeds contract DTO publication.

Initial changelog

  • v1 current: health, mode recommendation, resolve, feedback, billing checkout/portal/state, Stripe webhook integration
  • Current prelaunch additions: clientRef discipline, API key enforcement, billing gating, quota headers, runtime-backed billing state
  • Still evolving: final commercial language, legal text, security/privacy publication wording, backend hardening details

Draft pages

Draft

Pricing

Commercial packaging is not yet presented as final. The draft page explains this explicitly and points readers to contact the team.

Open draft pricing page

Draft

Security & Privacy

Current architectural and runtime facts are described, but this is not yet a final legal/security posture page.

Open draft security & privacy page

Draft

Terms / Privacy / Acceptable Use

Public policy wording still needs finalization. The draft page is clearly marked as provisional.

Open draft terms page

Draft

Status / Reliability

This page documents the cautious current operational posture without inventing an SLA.

Open draft status page