API Documentation
Integrate PenipuMY scam data into your apps, bots, and workflows.
The PenipuMY API provides programmatic access to Malaysia's community-driven scam database. Query phone numbers, bank accounts, social media profiles, and URLs against verified scam reports.
Authentication
All requests require an API key sent via the X-API-Key header.
curl -H "X-API-Key: YOUR_API_KEY" https://penipu.my/api/v1/stats
Generate your API key in Account Settings.
Rate Limits
Pricing effective: 14 September 2026
| Tier | Limit | Price |
|---|---|---|
| Free | 100 requests/day | Free |
| Starter | 10,000 requests/month | RM59/mo |
| Business | 50,000 requests/month | RM199/mo |
| Enterprise | 500,000 requests/month | RM999/mo |
| Bonus Quota | +5000 requests | RM30 (one-time) |
Bonus credits are used when your daily limit is exceeded and don't expire. Upgrade tiers or buy quota at Account Settings.
Free-tier limit resets at midnight MYT (UTC+8). Paid tiers reset monthly on your subscription's billing date (the day you subscribed), not a shared calendar date. Check response headers:
X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 997
Search
Search the scam database by phone number, bank account, social media, or general query.
Parameters
| Param | Type | Required | Description |
|---|---|---|---|
q | string | Required | Search query (min 3 chars). Phone number, bank account, name, or social username. |
type | string | Optional | auto (default), phone, bank, social, name |
limit | integer | Optional | Max results. Default: 10, Max: 25 |
Example Request
curl -H "X-API-Key: YOUR_KEY" "https://penipu.my/api/v1/search?q=0123456789"
import requests resp = requests.get( "https://penipu.my/api/v1/search", headers={"X-API-Key": "YOUR_KEY"}, params={"q": "0123456789", "type": "phone"} ) print(resp.json())
Response
{
"query": "0123456789",
"type": "phone",
"count": 1,
"results": [
{
"profile_id": "pid-abc123",
"name": "Suspect Name",
"total_reports": 5,
"total_loss_myr": 25000.0,
"asset_counts": {
"bank_accounts": 2,
"phone_numbers": 3,
"social_accounts": 1
},
"last_updated": "2026-02-28 12:00:00"
}
]
}
Profile Details
Get detailed information about a specific scam profile, including all linked reports.
Example Request
curl -H "X-API-Key: YOUR_KEY" "https://penipu.my/api/v1/profile/pid-abc123"
Response
{
"profile_id": "pid-abc123",
"name": "Suspect Name",
"total_reports": 5,
"total_loss_myr": 25000.0,
"asset_counts": {
"bank_accounts": 2,
"phone_numbers": 3,
"social_accounts": 1
},
"platforms_involved": ["Instagram", "TikTok"],
"reports": [
{
"report_id": 42,
"public_token": "aB3dEfGhIjKlMnOp",
"url": "https://penipu.my/report/view/aB3dEfGhIjKlMnOp",
"status": "VERIFIED",
"amount_lost_myr": 5000.0,
"submitted_at": "2026-01-15 09:30:00"
}
],
"created_at": "2025-06-01 10:00:00",
"last_updated": "2026-02-28 12:00:00"
}
Use url to link directly to a report — it uses public_token automatically when available. Linking off a raw report_id may stop working if report-link enumeration protection is enabled.
Phone Lookup
Quick lookup for a phone number. Returns police report count, community verified reports, and spam/fraud flags.
Parameters
| Param | Required | Description |
|---|---|---|
q | Required | Phone number (8-15 digits). Supports +60, 60, or 0 prefix. |
Example
curl -H "X-API-Key: YOUR_KEY" "https://penipu.my/api/v1/phone?q=0123456789"
Response
{
"phone": "0320260239",
"police_report_count": 0,
"police_report_status": "live",
"verified_report_count": 0,
"spam": false,
"fraud": false,
"business": {
"tier": "verified",
"brand_slug": "maybank",
"brand_name": "Maybank",
"branch_slug": "klcc",
"branch_name": "KLCC",
"display_name": "Maybank - KLCC",
"is_ambiguous": false,
"logo_url": "https://penipu.my/uploads/logo_maybank_8bb5199e.png",
"website": "https://www.maybank2u.com.my",
"place_url": "https://www.google.com/maps/place/...",
"address": "Lot G45, Suria KLCC, Kuala Lumpur",
"rating": 4.2,
"review_count": 186,
"opening_hours_status": "Open · Closes 12 am",
"wheelchair_accessible": true,
"scam_alert_banner": null
}
}
| Field | Type | Description |
|---|---|---|
phone | string | Normalized phone number |
police_report_count | integer | Number of PDRM police reports (via SemakMule) |
police_report_status | string | Source of police report count: live (fresh from SemakMule), cached (within 24h), cached_stale (older, SemakMule unreachable), unavailable (no cache and SemakMule down) |
verified_report_count | integer | Number of community verified reports |
spam | boolean | Flagged as spam by caller ID sources |
fraud | boolean | Flagged as fraud (verified reports or caller ID) |
business | object | null | Business directory match. Present when the phone belongs to a verified business (bank branch, merchant, etc.) or a compromised business number. null for unknown numbers. |
Business Object Fields
Returned when the phone matches a record in the business directory.
| Field | Type | Description |
|---|---|---|
business.tier | string | Trust tier: verified (legitimate business, admin-verified), verified_spoofing (verified business with ≥3 police reports — likely being spoofed by scammers), compromised (business number reported in active fraud), free (unverified directory listing). |
business.brand_slug | string | URL-safe brand identifier (e.g. maybank, cimb). |
business.brand_name | string | Brand display name. |
business.branch_slug | string | null | URL-safe branch identifier. null for brand-level numbers (e.g. customer hotlines). |
business.branch_name | string | null | Branch display name (e.g. KLCC, Customer Hotline). |
business.display_name | string | Pre-formatted "Brand - Branch" string for UI. |
business.is_ambiguous | boolean | true when the same phone serves multiple branches (e.g. customer service routing). |
business.logo_url | string | null | Full HTTPS URL to brand logo image. |
business.website | string | null | Branch-specific or brand primary website URL. |
business.place_url | string | null | Google Maps place URL for the branch. |
business.address | string | null | Branch postal address. |
business.rating | number | null | Google Maps rating (0.0-5.0). |
business.review_count | integer | null | Google Maps review count. |
business.opening_hours_status | string | null | Live-computed status (e.g. "Open · Closes 4 pm", "Closed · Opens 9:30 am", "Open 24 hours"). Computed in Malaysia time. |
business.wheelchair_accessible | boolean | null | Accessibility flag from Maps. null when unknown. |
business.scam_alert_banner | string | null | Active scam advisory banner text from admins (e.g. BNM alerts). null when none. |
business.spoofing_report_count | integer | Only present when tier == "verified_spoofing". Number of police reports against this verified business number — strong signal of impersonation/spoofing. |
Bank Account Lookup
Quick lookup for a bank account number. Returns police report count, community verified reports, and fraud flag.
Parameters
| Param | Required | Description |
|---|---|---|
q | Required | Bank account number (6-24 digits). Hyphens and spaces stripped automatically. |
Example
curl -H "X-API-Key: YOUR_KEY" "https://penipu.my/api/v1/bank?q=1234567890"
Response
{
"bank_account": "1234567890",
"holder_name": "AHMAD BIN ALI",
"bank_name": "Maybank Berhad",
"police_report_count": 2,
"police_report_status": "live",
"verified_report_count": 5,
"fraud": true
}
| Field | Type | Description |
|---|---|---|
bank_account | string | Normalized account number (digits only) |
holder_name | string | null | Account holder name from reports (null if not on file) |
bank_name | string | null | Bank name from reports (null if not on file) |
police_report_count | integer | Number of PDRM police reports (via SemakMule) |
police_report_status | string | Source of police report count: live (fresh from SemakMule), cached (within 24h), cached_stale (older, SemakMule unreachable), unavailable (no cache and SemakMule down) |
verified_report_count | integer | Number of community verified reports |
fraud | boolean | True if verified reports exist |
Platform Statistics
Get platform-wide statistics including total reports, profiles, and losses tracked.
Example Request
curl -H "X-API-Key: YOUR_KEY" "https://penipu.my/api/v1/stats"
Response
{
"total_profiles": 1234,
"total_reports": 5678,
"verified_reports": 3456,
"total_loss_myr": 2500000.0,
"total_bank_accounts_tracked": 890,
"total_phone_numbers_tracked": 1200,
"total_social_accounts_tracked": 450
}
Error Handling
The API uses standard HTTP status codes and returns JSON error responses.
| Status | Meaning |
|---|---|
400 | Bad request — missing or invalid parameters |
401 | Unauthorized — missing or invalid API key |
404 | Not found — profile does not exist |
429 | Rate limit exceeded — wait until midnight MYT |
500 | Server error |
All errors return JSON:
{
"error": "Query parameter "q" is required (min 3 characters)."
}
PenipuMY API v1 — penipu.my