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
clientReffor 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.
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.
| Parameter | Type | Notes |
|---|---|---|
country | query string | Preferred query parameter |
userCountry | query string | Also 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,searchLanguagecaptureMode:EXTENSIONorCHROMEsourceType:TEL_LINK,SELECTION,CONTEXT_MENU,PAGE_DETECTION,MANUAL,UNKNOWNoriginpage context fields including structural anchor fields when availablepreferencesfor 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_refin strict mode401 missing_api_key401 invalid_api_key403 client_ref_mismatch402 inactive_subscription429 plan_quota_exceeded400invalid request body
POST/v1/feedback/resolution
No API-key enforcement is implemented here in the current server file.
Request body fields:
resolutionIdaction:ACCEPTED_BEST_MATCH,ACCEPTED_ALTERNATIVE,SAVED_NUMBER_ONLY,REJECTED,EDITED_NAME_BEFORE_SAVE,ABANDONEDsavedAsName,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:
400unknown plan or invalid JSON503missing Stripe configuration502Stripe 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:
503portal configuration missing404no customer resolved yet502Stripe 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.completedcustomer.subscription.updatedcustomer.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:
- API key must be present
- API key must be known
- API key must match the same
clientRef - billing must be active for that
clientRef - quota evaluation is then applied using the server's current billing/usage interpretation
TRANSITIONAL vs STRICT
| Mode | Env var | Behavior when clientRef is missing |
|---|---|---|
| TRANSITIONAL | SAVE_CALL_REQUIRE_CLIENT_REF=false or unset | Request is allowed and response includes X-SaveCall-Warning: missing-client-ref |
| STRICT | SAVE_CALL_REQUIRE_CLIENT_REF=true | Request 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-PlanX-SaveCall-Quota-LimitX-SaveCall-Quota-UsedX-SaveCall-Quota-Remaining
X-SaveCall-Plan: unknown X-SaveCall-Quota-Limit: 50 X-SaveCall-Quota-Used: 37 X-SaveCall-Quota-Remaining: 13
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, witherror,code, andmessage
{
"error": true,
"code": "inactive_subscription",
"clientRef": "staging-tenant-001",
"message": "No active subscription for clientRef \"staging-tenant-001\""
}
Common codes:
missing_client_refmissing_api_keyinvalid_api_keyclient_ref_mismatchinactive_subscriptionplan_quota_exceeded
What integrators should do:
- treat
400as request-shape or strict-mode input issues - treat
401/403as auth or tenant-binding failures - treat
402as billing-state remediation needed - treat
429as quota exhaustion and surface quota headers if present - treat quota and plan identifiers as operational signals rather than final commercial labels
- log
debugTraceIdfrom 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.
Draft
Security & Privacy
Current architectural and runtime facts are described, but this is not yet a final legal/security posture page.
Draft
Terms / Privacy / Acceptable Use
Public policy wording still needs finalization. The draft page is clearly marked as provisional.
Draft
Status / Reliability
This page documents the cautious current operational posture without inventing an SLA.
SaveCall API docs