Every API here is available over the APIs.io API and to AI agents over MCP.
# Harvested verbatim from https://connect.plumma.it/docs/openapi/connect-api.json
# and serialized to YAML. Content is unmodified; see openapi/_original/ for the
# byte-for-byte JSON as fetched. Single tag (CONNECT), so no per-tag split applies.
openapi: 3.1.0
info:
title: Plumma CONNECT API
description: |
**Plumma CONNECT** is a phone-intelligence aggregation service. A single call
takes a phone number in E.164 form plus a list of **commands** (SIM-swap
check, KYC match, current carrier, porting timestamp, …), routes each command
to the best available supplier for the number's country, and returns one
aggregated response.
## Authentication
Every call must carry the `x-plumma-connect-api-key` header. Its value is your
**X.509 client certificate, base64-encoded**.
Two different refusals, and they call for two different actions:
- **`401`** — the certificate itself could not be used: unreadable, expired,
or not yet valid. A new key is what fixes it.
- **`403`** — the certificate is fine and the call is not allowed: the header
was not sent at all, or the certificate was issued for another purpose, or
the application it names is not one this key is admitted to. A new key
changes nothing here.
Both answer `application/problem+json`, and the `detail` field says which of
the cases it is in a sentence meant to be read.
## Live or sandbox: `x-plumma-connect-app-id` decides
The second header is what tells the two apart, and it is not optional in
practice — a live call without it does not fail, it silently becomes a
sandbox call.
- **Sent** — the call runs **live**: real suppliers, real data, billed per
served command. The pair (your certificate, that application) must be
**admitted**; if it is not, the call is refused with `403` and nothing is
billed. Admission is granted by Plumma per application, and an application
you have just created is not admitted until it is.
- **Not sent** — the call runs in the **sandbox**: canned identities, nothing
billed, and it may only ask about a **demo number**. Any other number is
refused with `401`.
The application id is the one shown for that application in the Plumma
console. There is no way to ask for the live engine any other way: the
request cannot name its own role.
## Billing (per served command)
You are charged **per command a supplier actually served**, never per API
call. A command whose country is not covered, whose supplier call failed, or
that was rejected before routing is **not** billed. A supplier answer that
means "I hold no data for this" (e.g. a KYC score of `-1`) is a served result
and **is** billed.
## Correlation id
Never send `X-Correlation-ID` — the server generates it and returns it on the
response header of every call (success or error). Quote it in support
requests: it identifies your call server-side.
## Model version
`info.version` matches the model set (PlmRequest / PlmResponse) this contract
describes. The **same** string is returned in the `version` field of every
`PlmResponse`, so you can confirm the service matches this document at runtime:
make any call and compare `response.version` with `info.version` here.
A standalone JSON Schema for request validation ships alongside this file at
`plmrequest.schema.json` — point your validator at it to check a payload
before sending.
contact:
name: PLUMMA SRL
email: info@plumma.it
license:
name: Proprietary — © PLUMMA SRL, all rights reserved.
version: 1.0.2-20260903172027
x-model-version: 1.0.2-20260903172027
servers:
- url: https://connect.plumma.it/services
description: Production
tags:
- name: CONNECT
description: The flagship aggregation endpoint.
paths:
/api:
post:
tags:
- CONNECT
summary: Run one or more intelligence commands against a phone number
operationId: connectApi
description: |
Submit a phone number and a non-empty list of commands. The response is a
single `PlmResponse` in which only the blocks relevant to the served
commands are populated (unpopulated fields are omitted, not null).
**Validation happens before routing.** A malformed payload returns `400`
with the offending field named in `detail`; no supplier is contacted and
nothing is billed.
security:
- ApiKeyAuth: []
parameters:
- $ref: '#/components/parameters/ApplicationId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PlmRequest'
examples:
simSwap:
summary: SIM-swap check (single command)
value:
number: '+393273339145'
commands:
- sim_swap
kycMatchStructured:
summary: KYC match with a structured address
value:
number: '+393273339145'
commands:
- kyc_match
kyc_challenges:
name:
first_name: Luigi
last_name: Armani
dob:
day: 28
month: 2
year: 1999
address:
street: Via Corsini
street_no: '21'
city: Fanano
province: MO
postcode: '41021'
country: IT
kycMatchInline:
summary: KYC match with a free-text (inline) address
value:
number: '+447808226974'
commands:
- kyc_match
kyc_challenges:
name:
first_name: John
last_name: Smith
inline_address:
address: 23 Omnia Street, London, EC1A 1BB, GB
normalize: true
wideCall:
summary: Several commands at once
value:
number: '+447808226974'
commands:
- current_carrier
- line_classification
- sim_swap
responses:
'200':
description: |
Routing completed. Inspect `status` / `status_message` for the outcome
and the populated blocks for the data. HTTP 200 does not by itself mean
every command was served — read `status`.
headers:
X-Correlation-ID:
$ref: '#/components/headers/CorrelationId'
content:
application/json:
schema:
$ref: '#/components/schemas/PlmResponse'
examples:
simSwapServed:
summary: SIM-swap served (the operator sent the date of the last swap)
value:
number: '+393273339145'
simswap:
last_day: 0
risk_indicator: 4
simswap_min_threshold: 720
date: '2018-10-17T23:00:00.000Z'
swapped: false
swapped_max_age: 240
status: 0
status_message: Response from one supplier
version: 1.0.2-20260704190444
simSwapNoSwapFound:
summary: SIM-swap served (0 = the operator watched and found no swap)
description: The operator holds no swap for this number, and sent no date to place one
with. `swapped` / `swapped_max_age` carry the bounded claim - nothing in the last
240 hours - while the absent thresholds say there is no swap to bound. Not to be confused
with `-1`, which is the operator having nothing to answer with at all.
value:
number: '+5581986179310'
simswap:
last_day: 0
risk_indicator: 0
swapped: false
swapped_max_age: 240
status: 0
status_message: Response from one supplier
version: 1.0.2-20260704190444
simSwapNotApplicable:
summary: SIM-swap served (-1 = the operator holds nothing for this identifier)
description: The number is well formed and the operator answered `422 SERVICE_NOT_APPLICABLE`
- a landline, an M2M or data-only SIM, an MVNO that has not enabled the API. An answer,
so the command is served and billed; a call that got no answer would leave the whole
`simswap` block absent instead.
value:
number: '+5519992858171'
simswap:
risk_indicator: -1
status: 0
status_message: Response from one supplier
version: 1.0.2-20260704190444
kycMatchServed:
summary: KYC match served (0 = no match, -1 = operator holds no data)
value:
number: '+393273339145'
kyc_results:
first_name_score: 0
last_name_score: 0
name_score: 0
dob_score: 0
country_score: -1
status: 0
status_message: Response from one supplier
version: 1.0.2-20260704190444
'400':
description: Malformed or invalid request payload — `detail` names the offending field.
headers:
X-Correlation-ID:
$ref: '#/components/headers/CorrelationId'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetail'
example:
type: about:blank
title: Bad Request
status: 400
detail: Plumma kyc_challenges must have either address or inline_address populated
instance: /services/api
path: /services/api
correlation_id: 8b3489e2-dab3-4b1f-a448-2ab46071fac8
'401':
description: |
The certificate could not be used: not a readable X.509 certificate,
or expired, or not yet valid. `detail` says which. Mint a new key.
headers:
X-Correlation-ID:
$ref: '#/components/headers/CorrelationId'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetail'
'403':
description: |
The certificate is usable and the call is still not allowed: the
`x-plumma-connect-api-key` header was absent, or the certificate was
issued for a purpose Connect does not accept, or it is not admitted to
the application named in `x-plumma-connect-app-id`. `detail` says
which. A new key does not help — the admission has to change.
headers:
X-Correlation-ID:
$ref: '#/components/headers/CorrelationId'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetail'
'500':
description: |
Unexpected server error. `detail` is generic by design — use the
`correlation_id` to locate the failure in the logs.
headers:
X-Correlation-ID:
$ref: '#/components/headers/CorrelationId'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetail'
components:
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: x-plumma-connect-api-key
description: Your X.509 client certificate, base64-encoded.
parameters:
ApplicationId:
name: x-plumma-connect-app-id
in: header
required: false
description: |
The application this call belongs to, as shown in the Plumma console.
**Present** — the call runs against the live suppliers as that
application, and the pair (certificate, application) has to be admitted:
it is refused with `403` otherwise, and a value that is not a number is
refused the same way.
**Absent** — the call runs in the sandbox and may only ask about a demo
number. Nothing is billed, and no admission is needed.
Omitting it is therefore not a smaller version of a live call: it is a
different call, answered from canned data.
schema:
type: string
pattern: ^[0-9]+$
example: '11'
headers:
CorrelationId:
description: Server-generated request id, echoed on every response. Never sent by the client.
schema:
type: string
format: uuid
example: 8b3489e2-dab3-4b1f-a448-2ab46071fac8
schemas:
ProblemDetail:
type: object
description: RFC 7807 problem document returned on every 4xx/5xx.
properties:
type:
type: string
format: uri
example: about:blank
title:
type: string
example: Bad Request
status:
type: integer
format: int32
example: 400
detail:
type: string
example: Plumma commands list cannot be null or empty
instance:
type: string
example: /services/api
path:
type: string
example: /services/api
correlation_id:
type: string
format: uuid
example: 8b3489e2-dab3-4b1f-a448-2ab46071fac8
Address:
type: object
properties:
street:
type: string
minLength: 1
maxLength: 100
examples:
- Via Corsini
street_no:
type: string
maxLength: 10
pattern: '[A-Za-z0-9\-/]+'
examples:
- '21'
city:
type: string
minLength: 1
maxLength: 50
examples:
- Fanano
$comment: Server enforces [\p{L}'\-\s]+ ; omitted here for cross-engine portability.
province:
type: string
maxLength: 50
examples:
- MO
postcode:
type: string
pattern: '[0-9A-Za-z]{4,10}'
examples:
- '41021'
national_id:
type: string
maxLength: 50
pattern: '[A-Za-z0-9\-]+'
country:
type: string
pattern: '[A-Za-z]{2,3}'
description: ISO country code (2-3 letters).
examples:
- IT
normalize:
type: boolean
default: false
house_number_extension:
type: string
additionalProperties: false
description: Structured postal address. Mutually exclusive with inline_address.
required:
- street
- street_no
- city
- postcode
- country
Dob:
type: object
properties:
day:
type: integer
minimum: 1
maximum: 31
examples:
- 28
month:
type: integer
minimum: 1
maximum: 12
examples:
- 2
year:
type: integer
minimum: 1900
maximum: 2100
examples:
- 1999
additionalProperties: false
description: Date of birth. Either fully specified (day+month+year) or entirely omitted — a partial
DOB is rejected.
dependencies:
day:
- month
- year
month:
- day
- year
year:
- day
- month
InlineAddress:
type: object
properties:
address:
type: string
minLength: 1
examples:
- 23 Omnia Street, London, EC1A 1BB, GB
normalize:
type: boolean
default: false
additionalProperties: false
description: Free-text address as a single string. Mutually exclusive with address.
required:
- address
KycChallenges:
type: object
properties:
dob:
$ref: '#/components/schemas/Dob'
name:
$ref: '#/components/schemas/Name'
address:
$ref: '#/components/schemas/Address'
email:
type: string
format: email
examples:
- john.smith@example.com
inline_address:
$ref: '#/components/schemas/InlineAddress'
return_address:
type: boolean
description: Ask the supplier to return the normalized address block.
gender:
type: string
additionalProperties: false
description: 'Data to match for `kyc_match`. Must carry EXACTLY ONE address form: `address` (structured)
or `inline_address` (free text) — never both, never neither.'
oneOf:
- $comment: structured address present, inline absent
required:
- address
not:
required:
- inline_address
- $comment: inline address present, structured absent
required:
- inline_address
not:
required:
- address
Name:
type: object
properties:
first_name:
type: string
minLength: 2
maxLength: 100
$comment: Server also requires ^[\p{L}][\p{L}'\-\s]*$; \p{L} is not portable across JSON Schema
regex engines, so only length is validated here and the exact character rule stays server-side.
last_name:
type: string
minLength: 2
maxLength: 100
middle_name:
type: string
minLength: 2
maxLength: 100
name_kana_hankaku:
type: string
name_kana_zenkaku:
type: string
family_name_at_birth:
type: string
minLength: 2
maxLength: 100
additionalProperties: false
allowedCommandValues:
type: string
enum:
- line_classification
- current_carrier
- issuing_carrier
- porting_timestamp
- porting_logs
- network_presence
- roaming_intel
- deactivation_point
- churn_tracker
- sim_swap
- port_fraud_shield
- digital_footprint
- age_verification
- kyc_match
- divert_detector
- commercial_segment
- tenure_period
PlmRequest:
type: object
properties:
number:
type: string
minLength: 1
pattern: ^\+?[1-9][0-9]{6,14}$
description: Phone number in international form. Accepted with or without a leading '+'; the
server normalises to E.164 and rejects numbers libphonenumber considers invalid (a regex cannot
fully replicate that check).
examples:
- '+393273339145'
- '447808226974'
commands:
type: array
items:
$ref: '#/components/schemas/allowedCommandValues'
minItems: 1
description: Non-empty list of commands to run. Duplicate/unknown values are rejected (400).
kyc_challenges:
$ref: '#/components/schemas/KycChallenges'
tenure_period:
type: integer
description: Tenure window in days. Required only when the `tenure_period` command is requested.
examples:
- 90
additionalProperties: false
required:
- number
- commands
allOf:
- $comment: When kyc_match is requested, kyc_challenges is required.
if:
properties:
commands:
contains:
const: kyc_match
required:
- commands
then:
required:
- kyc_challenges
- $comment: When the tenure_period command is requested, the tenure_period value is required.
if:
properties:
commands:
contains:
const: tenure_period
required:
- commands
then:
required:
- tenure_period
AgeVerification:
type: object
properties:
verified:
type: integer
threshold:
type: integer
ChurnTracker:
type: object
properties:
operator:
type: string
action:
type: string
ts:
type: string
format: date-time
description: When the deactivation/churn event happened, ISO-8601 UTC.
examples:
- '2025-11-02T08:30:00.000Z'
number:
type: integer
number2:
type: integer
CurrentCarrier:
type: object
properties:
lrn: {}
mcc:
type: string
mnc:
type: string
name:
type: string
spid:
type: string
ocn: {}
DigitalFootprint:
type: object
properties:
whatsapp:
type: integer
telegram:
type: integer
amazon:
type: integer
google:
type: integer
office365:
type: integer
instagram:
type: integer
linkedin:
type: integer
twitter:
type: integer
skype:
type: integer
flipkart:
type: integer
viber:
type: integer
bukalapak:
type: integer
facebook:
type: integer
error:
type: integer
error_description:
type: string
DivertDetector:
type: object
properties:
unconditional_call_forward:
type: integer
IssuingCarrier:
type: object
properties:
mcc:
type: string
mnc:
type: string
name:
type: string
spid:
type: string
ocn: {}
KycResults:
type: object
properties:
first_name_score:
type: integer
last_name_score:
type: integer
middle_name_score:
type: integer
name_score:
type: integer
address_score:
type: integer
street_no_score:
type: integer
street_score:
type: integer
city_score:
type: integer
postcode_score:
type: integer
dob_score:
type: integer
province_score:
type: integer
country_score:
type: integer
national_id_score:
type: integer
email_score:
type: integer
NormalizedAddress:
type: object
properties:
street_no:
type: string
street:
type: string
city:
type: string
province:
type: string
postcode:
type: string
country:
type: string
PortingLogs:
type: object
properties:
plmnetwork:
type: integer
i_type:
type: string
action:
type: string
ts:
type: string
format: date-time
description: When the porting event happened, ISO-8601 UTC.
examples:
- '2016-03-11T09:15:00.000Z'
QdrHistory:
type: object
properties:
service:
type: string
source:
type: string
ts:
type: string
format: date-time
description: When the query-detail record was observed, ISO-8601 UTC.
examples:
- '2026-08-01T07:00:00.000Z'
plmnetwork:
type: string
RoamingNetwork:
type: object
properties:
lrn: {}
mcc:
type: string
mnc:
type: string
name:
type: string
spid:
type: string
ocn: {}
Simswap:
type: object
properties:
last_day:
type: integer
description: '1 when the last SIM change falls within the last 24 hours, 0 otherwise. Absent
when neither can be stated: when the operator answered only that a swap happened somewhere
inside a window wider than 24 h, and when it refused or held nothing.'
risk_indicator:
type: integer
description: '0 = the operator watched and found no SIM swap, the lowest risk there is. 1 =
swap in the last 24 h, 2 = last 72 h, 3 = last 30 days, 4 = older than 30 days, which is stated
only when the operator''s own monitored period is at least that long. Two negatives are answers,
not failures: -1 = the operator answered that it holds nothing servable for this number, -2
= the operator answered refusing on this number. A command whose call got no answer leaves
the whole block absent instead - which is the difference between an operator with nothing
to say and a supplier that never replied.'
examples:
- 0
simswap_min_threshold:
type: integer
description: Lower bound, in hours, of the window the last SIM change falls into. Absent when
there is no lower bound (the change is more recent than 24 h) and when there is no swap to
bound at all (`risk_indicator` 0).
simswap_max_threshold:
type: integer
description: Upper bound, in hours, of the window the last SIM change falls into. Absent when
there is no upper bound (the change is older than the monitored period) and when there is
no swap to bound at all (`risk_indicator` 0). When the operator answered with the boolean
alone, this is the window that boolean referred to.
date:
type: string
format: date-time
description: When the last SIM change happened, ISO-8601 UTC. Absent when the supplier answered
but holds no date.
examples:
- '2018-10-17T23:00:00.000Z'
swapped:
type: boolean
description: Whether a SIM swap happened within the last `swapped_max_age` hours. Meaningless
without that window, which is why the two always travel together. Absent when neither the
date nor the supplier's own answer allowed it to be determined.
examples:
- false
swapped_max_age:
type: integer
description: The window, in hours, that `swapped` refers to.
examples:
- 240
description: SIM-swap block. `date` and the `risk_indicator` / threshold trio are the supplier's
own view of the last SIM change; `swapped` / `swapped_max_age` are the CAMARA pair and answer
one fixed question, so two suppliers can be compared.
PlmResponse:
type: object
properties:
number:
type: string
kyc_results:
$ref: '#/components/schemas/KycResults'
normalize_address:
type: string
current_carrier:
$ref: '#/components/schemas/CurrentCarrier'
ported:
type: boolean
issuing_carrier:
$ref: '#/components/schemas/IssuingCarrier'
ported_date:
type: string
format: date-time
description: When the number was ported to its current carrier, ISO-8601 UTC. Suppliers that
only hold the calendar day are rendered at midnight UTC; `ported_date_type` declares how precise
the underlying date is.
examples:
- '2016-03-11T00:00:00.000Z'
ported_date_type:
type: string
description: How precise `ported_date` is, as declared by the supplier that answered.
examples:
- FULL
present:
type: string
type:
type: string
etype:
type: string
porting_logs:
type: array
items:
$ref: '#/components/schemas/PortingLogs'
description: Porting events known for the number. Absent when the command was not requested
or no supplier held any row — never an empty array.
is_roaming:
type: string
roaming_network:
$ref: '#/components/schemas/RoamingNetwork'
portfraud:
type: string
qdr_history:
type: array
items:
$ref: '#/components/schemas/QdrHistory'
status:
type: integer
status_message:
type: string
simswap:
$ref: '#/components/schemas/Simswap'
plm_score:
type: integer
age_verification:
$ref: '#/components/schemas/AgeVerification'
digital_footprint:
$ref: '#/components/schemas/DigitalFootprint'
divert_detector:
$ref: '#/components/schemas/DivertDetector'
churn_tracker:
type: array
items:
$ref: '#/components/schemas/ChurnTracker'
description: Deactivation/churn events known for the number, most useful as a recency signal.
Absent when the command was not requested or no supplier held any row — never an empty array.
normalized_address:
$ref: '#/components/schemas/NormalizedAddress'
market_segment:
type: string
version:
type: string
readOnly: true
description: The model-set version that produced this response. Compare it with `info.version`
of this document to confirm the service matches the contract you are reading.