Developers
One endpoint. Twenty-two typed commands. One response shape.
POST a number and the commands you want. You get a block for each one that was served. Commands that were not served are named in the response.
POST /api
one operation, one authorisation flow, one response envelope
mTLS
an X.509 client certificate, base64 encoded, in a single header
1 header
key, used both for testing against the demo numbers and for live traffic
RFC 9457
errors as problem+json, each one carrying a correlation id
The request path
The request path, from your POST to the response.
Authentication, routing, normalisation and the record happen behind the endpoint. Your integration carries none of it.
One call, start to finish
Three headers, one POST, and the two shapes that come back.
The whole walkthrough is on this panel. Move through it in order, or go straight to the error case, which is the one most integrations actually need on day two.
Your certificate is your key.
Your X.509 client certificate, base64 encoded, in one header. A second header names the application, and the application decides sandbox or live. You promote an integration by changing one header value.
# Your X.509 client certificate, base64 encoded x-plumma-connect-api-key: MIIDXTCCAkWgAwIBAgIJAJ… # Application id. Controls sandbox or live. x-plumma-connect-app-id: 10427 Content-Type: application/json
A number, and the questions you want answered about it.
E.164 number, at least one command, as many as the decision needs in one round trip. kyc_match takes the attributes to check and tenure_period a window in days.
{
"number": "+393401234567",
"commands": [
"sim_swap",
"divert_detector",
"current_carrier",
"kyc_match"
],
"kyc_challenges": {
"name": { "first_name": "Giulia", "last_name": "Rossi" },
"dob": { "day": 14, "month": 3, "year": 1991 },
"address": {
"street": "Via Rizzoli",
"city": "Bologna",
"postcode": "40125",
"country": "IT"
}
}
}
Only the blocks that were actually served.
One block per command that answered. A command nobody could serve is absent, so an unserved check never reaches your risk engine dressed as a negative finding. Every response names the carrier and returns X-Correlation-ID.
{
"number": "+393401234567",
"status": 0,
"status_message": "OK",
"version": "2026.09",
"simswap": {
"swapped": true,
"date": "2026-09-20T22: 14: 03Z",
"last_day": 2,
"risk_indicator": "high"
},
"divert_detector": { "unconditional_call_forward": true },
"current_carrier": { "name": "Test Network", "mcc": "001", "mnc": "01" },
"kyc_results": {
"name_score": 98,
"dob_score": 100,
"country_score": 100
}
}
Problem details, with an identifier you can quote.
RFC 9457 problem details. A malformed payload names the offending field, so the fix is obvious from the response. 400 bad request, 401 the certificate is unreadable or expired, 403 the application is not admitted, 500 quote the correlation id.
{
"type": "https://plumma.it/problems/bad-request",
"title": "Bad Request",
"status": 400,
"detail": "commands must contain at least one item",
"instance": "/api",
"path": "$.commands",
"correlation_id": "9f2c41e8-3a77-4c51-b0d9-7e1f4a6c2b8d"
}
What it replaces
A thousand endpoints, or twenty-two commands.
Command reference
The twenty enum values, and the block each one returns.
Any combination, one request. Every block schema is in the OpenAPI description, and this table is read from it.
| Command | Response block | What it tells you |
|---|---|---|
| sim_swap | simswap | Whether the SIM behind the number changed inside the fixed 240-hour window. |
| port_fraud_shield | portfraud | Porting activity that fits an account takeover in progress. |
| divert_detector | divert_detector | Whether unconditional call forwarding is set on the line. |
| digital_footprint | digital_footprint | Where the number shows up across online platforms. |
| device_swap | device_swap | Whether the handset behind the number changed, and when. |
| location_verification | location_verification | Whether the device is inside an area you declare, with a match rate. |
| kyc_match | kyc_results | Whether the details given match the operator's own record, attribute by attribute. |
| age_verification | age_verification | That the line holder is over a threshold, with no document and no camera step. |
| tenure_period | status fields | Whether the line has been held longer than the period you ask about. |
| commercial_segment | market_segment | A consumer line from a business line. |
| network_presence | status fields | Whether the line is live and reachable right now. |
| roaming_intel | is_roaming, roaming_network | Whether the line is roaming, and on which network. |
| deactivation_point | churn_tracker | When the line was last deactivated. |
| line_classification | status fields | Mobile, landline and VoIP, told apart. |
| location_retrieval | device_location | Where the device last reported itself, as an area rather than a point. |
| current_carrier | current_carrier | The operator serving the line today: name, MCC, MNC, SPID, OCN, LRN. |
| issuing_carrier | issuing_carrier | The operator that originally issued the number. |
| porting_timestamp | ported, ported_date | Whether the number has ported, and when. |
| porting_logs | porting_logs | The porting events behind that date. |
| churn_tracker | churn_tracker | The line's history of deactivations, swaps, suspensions and reactivations. |
Availability is per operator and per market. A command that no path can serve returns no block, and the call is not billed.
Before you build
Three things that will otherwise cost you a sprint.
Absence is information
Write your rules so a missing block routes to the fallback you designed, never so it reads as a clean result.
The prefix does not identify the operator
Number portability means routing is resolved per call, which is why the served carrier is returned rather than assumed.
Session-bound checks need a second path
Any check proving the session runs on the claimed SIM uses the authorisation code flow over mobile data and fails on Wi-Fi.
Sandbox keys, schemas and the reference.
The developer portal holds the OpenAPI description, the normalised response shapes, the per-command fallback documentation and the sandbox. Nothing on it sits behind a form.