openapi: 3.2.0 info: title: Plumma CONNECT API description: '**Plumma CONNECT** is a phone-intelligence aggregation service.' 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: schemas: 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. 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 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 RoamingNetwork: type: object properties: lrn: {} mcc: type: string mnc: type: string name: type: string spid: type: string ocn: {} 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 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 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 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 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 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. 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 NormalizedAddress: type: object properties: street_no: type: string street: type: string city: type: string province: type: string postcode: type: string country: type: string 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 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 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 CurrentCarrier: type: object properties: lrn: {} mcc: type: string mnc: type: string name: type: string spid: type: string ocn: {} 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 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: {} AgeVerification: type: object properties: verified: type: integer threshold: type: integer 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' 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 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' securitySchemes: ApiKeyAuth: type: apiKey in: header name: x-plumma-connect-api-key description: Your X.509 client certificate, base64-encoded.