# SMSLovers Public API v1 Public guide: https://smslovers.com/developers/ OpenAPI: https://smslovers.com/api-openapi.json Base URL: https://smslovers.com/api/v1 Authentication: Authorization: Bearer (server-side only) Discounted API rates: save 25–50%, depending on availability. Live quotes apply a cost and margin floor; not all stock is eligible. Add balance and contact support on the website or in Discord; deposits, promos and tickets are not available through the API. Resends are free to customers. Amounts: integer EUR cents. Dates: Unix milliseconds. Responses: {data,requestId}; errors: {error:{code,message,retryAfterSeconds?},requestId}. Poll orders every 5–10 seconds. Preserve idempotency keys and exact bodies on retries. Keys are revoked on password reset/change and account merging. Keys have scopes, expiry, revocation and daily gross SMS caps (UTC), including later refunded reservations. Ten active keys, five active orders per account. Limits: 240 GET/min and 60 POST/min per key, 600 requests/min per account; quotes30/min, purchases10/min, cancel10/min and resend operations10/min per account. Webhook configuration20/hour; tests5/hour per account. Ingress1800/min per IP. Honor Retry-After; checks and payments run independently. No CORS or subaccounts. Signed outgoing webhooks support balance.low, sms.received, sms.ended, sms.review, deposit.credited; configure at /webhooks or in the dashboard. ## GET /webhooks — List webhook endpoints and event types Scope: webhooks:read Lists active/paused endpoints, never signing secrets. Up to five configured endpoints per account. Response data schema: {"type":"object","properties":{"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}},"events":{"type":"array","items":{"type":"string","enum":["balance.low","sms.received","sms.ended","sms.review","deposit.credited"]}}},"required":["webhooks","events"],"additionalProperties":false} ## POST /webhooks — Create an event receiver Scope: webhooks:write Also requires account:read for balance.low, sms:read for SMS events, account:read for deposit.credited. URLs are unique per account; retrying creation returns 409, so list endpoints before creating again. Rotate the secret if a creation response was lost. If already below the threshold, one initial low-balance event is queued. Request schema: {"type":"object","properties":{"name":{"type":"string","description":"Endpoint name.","minLength":2,"maxLength":60},"events":{"type":"array","minItems":1,"maxItems":5,"uniqueItems":true,"items":{"type":"string","enum":["balance.low","sms.received","sms.ended","sms.review","deposit.credited"]}},"lowBalanceCents":{"type":"integer","description":"Low-balance threshold in EUR cents. Defaults to 500 (€5).","minimum":1,"maximum":100000},"enabled":{"type":"boolean","default":true},"url":{"type":"string","description":"Public HTTPS domain on port 443. No IP literals, credentials, fragments or private DNS destinations.","format":"uri","maxLength":2048}},"required":["name","url","events"],"additionalProperties":false} Example: {"name":"My product","url":"https://api.example.com/webhooks/smslovers","events":["balance.low","sms.received"],"lowBalanceCents":500,"enabled":true} Response data schema: {"type":"object","properties":{"webhook":{"$ref":"#/components/schemas/Webhook"},"secret":{"type":"string","description":"Signing secret, shown once. Store privately."}},"required":["webhook","secret"],"additionalProperties":false} ## POST /webhooks/{id} — Update or pause a webhook Scope: webhooks:write Partial update. Requires the read scopes for every subscribed event. The URL is immutable: create a new endpoint to change it. Pausing stops queued attempts; an in-flight request may complete. Request schema: {"type":"object","properties":{"name":{"type":"string","description":"Endpoint name.","minLength":2,"maxLength":60},"events":{"type":"array","minItems":1,"maxItems":5,"uniqueItems":true,"items":{"type":"string","enum":["balance.low","sms.received","sms.ended","sms.review","deposit.credited"]}},"lowBalanceCents":{"type":"integer","description":"Low-balance threshold in EUR cents. Defaults to 500 (€5).","minimum":1,"maximum":100000},"enabled":{"type":"boolean","default":true}},"required":[],"additionalProperties":false} Example: {"enabled":false} Parameters: [{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"SMSLovers UUID.","format":"uuid"}}] Response data schema: {"type":"object","properties":{"webhook":{"$ref":"#/components/schemas/Webhook"}},"required":["webhook"],"additionalProperties":false} ## GET /webhooks/{id}/deliveries — Inspect webhook deliveries Scope: webhooks:read 50 entries per page, newest first. Logs contain metadata, never signing secrets, SMS text or raw response bodies. Retained for 30 days. Parameters: [{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"SMSLovers UUID.","format":"uuid"}},{"name":"page","in":"query","schema":{"type":"integer","minimum":0,"maximum":999999,"default":0},"description":"Zero-based page."}] Response data schema: {"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/WebhookDelivery"}},"page":{"type":"integer","description":"Zero-based page."},"limit":{"type":"integer","const":50}},"required":["items","page","limit"],"additionalProperties":false} ## POST /webhooks/{id}/rotate-secret — Rotate the signing secret Scope: webhooks:write Also requires the subscribed event read scopes. Effective immediately for new attempts. An already submitted delivery may use the old secret. Update your verifier and allow a short transition window where appropriate. Request schema: {"type":"object","properties":{},"required":[],"additionalProperties":false} Example: {} Parameters: [{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"SMSLovers UUID.","format":"uuid"}}] Response data schema: {"type":"object","properties":{"secret":{"type":"string","description":"New secret, shown once."}},"required":["secret"],"additionalProperties":false} ## POST /webhooks/{id}/test — Queue a signed test event Scope: webhooks:write Sends webhook.test with a harmless message to this enabled endpoint. HTTP 202 means queued, not delivered. Five tests per hour per account and 30 per minute service-wide. Request schema: {"type":"object","properties":{},"required":[],"additionalProperties":false} Example: {} Parameters: [{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"SMSLovers UUID.","format":"uuid"}}] Response data schema: {"type":"object","properties":{"queued":{"type":"boolean"},"eventId":{"type":"string","description":"Test event ID."}},"required":["queued","eventId"],"additionalProperties":false} ## POST /webhooks/{id}/retry — Retry a failed delivery Scope: webhooks:write Also requires the subscribed event read scopes. Only failed deliveries on an enabled endpoint can be retried. Reuses the same event ID, delivery ID and payload; the signature timestamp is fresh. HTTP 202. Request schema: {"type":"object","properties":{"deliveryId":{"type":"string","description":"ID from delivery logs."}},"required":["deliveryId"],"additionalProperties":false} Example: {"deliveryId":"whd_0123456789abcdef0123456789abcdef"} Parameters: [{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"SMSLovers UUID.","format":"uuid"}}] Response data schema: {"type":"object","properties":{"queued":{"type":"boolean"}},"required":["queued"],"additionalProperties":false} ## POST /webhooks/{id}/remove — Remove a receiver Scope: webhooks:write Stops queued deliveries and hides the endpoint. An in-flight request may complete. Registering the same URL later generates a new secret. Request schema: {"type":"object","properties":{},"required":[],"additionalProperties":false} Example: {} Parameters: [{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"SMSLovers UUID.","format":"uuid"}}] Response data schema: {"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"],"additionalProperties":false} ## GET /account — Account and balance Scope: account:read One shared EUR balance across the website, Discord and API. Key caps reset at 00:00 UTC; spentTodayCents counts gross SMS reservations, including subsequently refunded orders. Response data schema: {"$ref":"#/components/schemas/Account"} ## GET /ledger — Balance ledger Scope: account:read Newest first. Includes reservations, refunds, confirmed deposits and bonuses. Parameters: [{"name":"page","in":"query","schema":{"type":"integer","minimum":0,"maximum":999999,"default":0},"description":"Zero-based page."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Results per page."}] Response data schema: {"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Ledger"}},"page":{"type":"integer","description":"Current page."},"limit":{"type":"integer","description":"Page size."},"nextPage":{"type":["integer","null"],"description":"Next page, or null at the end."}},"required":["items","page","limit","nextPage"],"additionalProperties":false} ## GET /countries — Country keys Scope: sms:read Use key exactly as returned, such as US or Germany. Country keys are not universally ISO codes. Response data schema: {"type":"array","items":{"$ref":"#/components/schemas/Country"}} ## GET /services — Search services Scope: sms:read Alphabetical. Country lists describe coverage, not guaranteed live stock. Parameters: [{"name":"page","in":"query","schema":{"type":"integer","minimum":0,"maximum":999999,"default":0},"description":"Zero-based page."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Results per page."},{"name":"q","in":"query","schema":{"type":"string","description":"Case-insensitive name search.","maxLength":100}},{"name":"country","in":"query","schema":{"type":"string","description":"Exact country key, e.g. US."}}] Response data schema: {"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Service"}},"page":{"type":"integer","description":"Current page."},"limit":{"type":"integer","description":"Page size."},"nextPage":{"type":["integer","null"],"description":"Next page, or null at the end."},"total":{"type":"integer","description":"Number of matching services."}},"required":["items","page","limit","nextPage"],"additionalProperties":false} ## GET /services/{id} — Service and supported countries Scope: sms:read Parameters: [{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1}}] Response data schema: {"type":"object","properties":{"id":{"type":"integer","description":"Service ID."},"name":{"type":"string","description":"Service name."},"countries":{"type":"array","items":{"$ref":"#/components/schemas/Country"}}},"required":["id","name","countries"],"additionalProperties":false} ## POST /quotes — Get a discounted live quote Scope: sms:write Quotes expire after 60 seconds and belong to this account, API key and API billing channel. Select an options[].id. Standard and Resend choices are returned only when available. available:false means no eligible discounted stock; try another service/country later. Quoting never spends balance. Request schema: {"type":"object","properties":{"serviceId":{"type":"integer","description":"ID from /services.","minimum":1},"country":{"type":"string","description":"Country key from /countries."}},"required":["serviceId","country"],"additionalProperties":false} Example: {"serviceId":1,"country":"US"} Response data schema: {"$ref":"#/components/schemas/Quote"} ## POST /orders — Buy an SMS number Scope: sms:write Requires Idempotency-Key. Reserves priceCents from your balance once. Retry the exact quoteId with the same key, including after a timeout or quote expiry. Changing the quoteId with the same key returns 409. Five active purchases per account across all channels. reconciling means do not buy a replacement blindly; contact support. Request schema: {"type":"object","properties":{"quoteId":{"type":"string","description":"SMSLovers UUID.","format":"uuid"}},"required":["quoteId"],"additionalProperties":false} Example: {"quoteId":"11111111-1111-4111-8111-111111111111"} Parameters: [{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","description":"16–80 letters, numbers or hyphens. Generate once and persist for this operation.","pattern":"^[-a-zA-Z0-9]{16,80}$"}}] Response data schema: {"$ref":"#/components/schemas/Order"} ## GET /orders — List your SMS orders Scope: sms:read Includes your website and Discord SMS orders too. status filters the stored lifecycle (not the derived resending/resend_review presentation). Parameters: [{"name":"page","in":"query","schema":{"type":"integer","minimum":0,"maximum":999999,"default":0},"description":"Zero-based page."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Results per page."},{"name":"status","in":"query","schema":{"type":"string","enum":["purchasing","waiting","received","cancel_requested","cancelled","refunded","unavailable","reconciling"]}}] Response data schema: {"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Order"}},"page":{"type":"integer","description":"Current page."},"limit":{"type":"integer","description":"Page size."},"nextPage":{"type":["integer","null"],"description":"Next page, or null at the end."}},"required":["items","page","limit","nextPage"],"additionalProperties":false} ## GET /orders/{id} — Get the number, code and history Scope: sms:read Poll every 5–10 seconds. This reads the current state; automatic background checks receive new SMS messages. Stop polling terminal orders. GET never places a new order or requests a resend. Parameters: [{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"SMSLovers UUID.","format":"uuid"}}] Response data schema: {"$ref":"#/components/schemas/Order"} ## POST /orders/{id}/cancel — Cancel an unused number Scope: sms:write Available one minute after purchase while waiting. A confirmed cancellation or unused-number refund returns exactly the charged amount. Cancellation is naturally idempotent; a used or uncertain order returns 409. Request schema: {"type":"object","properties":{},"required":[],"additionalProperties":false} Example: {} Parameters: [{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"SMSLovers UUID.","format":"uuid"}}] Response data schema: {"$ref":"#/components/schemas/Order"} ## POST /orders/{id}/resend-check — Check resend availability Scope: sms:write After the first code. A live check may say unavailable even when a Resend option was selected earlier; availability and expiry can change. Does not request a new SMS. Request schema: {"type":"object","properties":{},"required":[],"additionalProperties":false} Example: {} Parameters: [{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"SMSLovers UUID.","format":"uuid"}}] Response data schema: {"$ref":"#/components/schemas/Order"} ## POST /orders/{id}/resend — Request another SMS Scope: sms:write Requires Idempotency-Key. Free to the customer; no per-resend balance debit. Request the next code on the service you are verifying, then poll the same order ID. Previous codes remain in messages. Up to 25 resend attempts per number; actual eligibility and expiry vary. For a definitive 409, check availability before choosing a new idempotency key. For a timeout/5xx, retain the original key. Request schema: {"type":"object","properties":{},"required":[],"additionalProperties":false} Example: {} Parameters: [{"name":"id","in":"path","required":true,"schema":{"type":"string","description":"SMSLovers UUID.","format":"uuid"}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","description":"16–80 letters, numbers or hyphens. Generate once and persist for this operation.","pattern":"^[-a-zA-Z0-9]{16,80}$"}}] Response data schema: {"$ref":"#/components/schemas/Order"} ## Schemas { "Webhook": { "type": "object", "properties": { "id": { "type": "string", "description": "SMSLovers UUID.", "format": "uuid" }, "name": { "type": "string", "description": "Endpoint label." }, "url": { "type": "string", "description": "Public HTTPS receiver URL." }, "events": { "type": "array", "items": { "type": "string", "enum": [ "balance.low", "sms.received", "sms.ended", "sms.review", "deposit.credited" ] } }, "lowBalanceCents": { "type": "integer", "description": "Alert threshold in EUR cents." }, "enabled": { "type": "boolean" }, "createdAt": { "type": "integer", "description": "Unix milliseconds." }, "updatedAt": { "type": "integer", "description": "Unix milliseconds." } }, "required": [ "id", "name", "url", "events", "lowBalanceCents", "enabled", "createdAt", "updatedAt" ], "additionalProperties": false }, "WebhookDelivery": { "type": "object", "properties": { "id": { "type": "string", "description": "Stable delivery ID." }, "type": { "type": "string", "description": "Event type." }, "status": { "type": "string", "enum": [ "queued", "delivered", "failed" ] }, "attempts": { "type": "integer", "description": "Attempts made." }, "responseStatus": { "type": [ "integer", "null" ], "description": "Last HTTP status, or null for network failure." }, "lastError": { "type": [ "string", "null" ], "description": "Generic failure information." }, "createdAt": { "type": "integer", "description": "Unix milliseconds." }, "deliveredAt": { "type": [ "integer", "null" ], "description": "Unix milliseconds." }, "nextAttemptAt": { "type": "integer", "description": "Next scheduled attempt, Unix milliseconds." } }, "required": [ "id", "type", "status", "attempts", "responseStatus", "lastError", "createdAt", "deliveredAt", "nextAttemptAt" ], "additionalProperties": false }, "WebhookEvent": { "type": "object", "properties": { "id": { "type": "string", "description": "Stable event ID; use for idempotency." }, "type": { "type": "string", "enum": [ "balance.low", "sms.received", "sms.ended", "sms.review", "deposit.credited", "webhook.test" ] }, "accountId": { "type": "string", "description": "SMSLovers UUID.", "format": "uuid" }, "createdAt": { "type": "integer", "description": "Unix milliseconds." }, "data": { "type": "object", "description": "balance.low: currency, balanceCents, previousBalanceCents, thresholdCents. sms.*: order {id,serviceId,service,country,priceCents,phone,status,expiresAt,resendId?}; sms.received also includes code and message. deposit.credited: deposit {id,method,asset,creditCents,creditedAt,txid?,amount?,amountCents?}, balanceCents. webhook.test: message." } }, "required": [ "id", "type", "accountId", "createdAt", "data" ], "additionalProperties": false }, "Country": { "type": "object", "properties": { "key": { "type": "string", "description": "Exact country key." }, "name": { "type": "string", "description": "Country name." }, "dialCode": { "type": "integer", "description": "Calling prefix; e.g. 1 or 49." } }, "required": [ "key", "name", "dialCode" ], "additionalProperties": false }, "Service": { "type": "object", "properties": { "id": { "type": "integer", "description": "Service ID." }, "name": { "type": "string", "description": "Service name." }, "countries": { "type": "array", "items": { "type": "string", "description": "Country key." } } }, "required": [ "id", "name", "countries" ], "additionalProperties": false }, "Key": { "type": "object", "properties": { "id": { "type": "string", "description": "SMSLovers UUID.", "format": "uuid" }, "name": { "type": "string", "description": "Key label." }, "prefix": { "type": "string", "description": "Non-secret prefix; full key is never returned." }, "scopes": { "type": "array", "items": { "type": "string", "description": "Granted permission." } }, "dailyLimitCents": { "type": "integer", "description": "Daily gross reservation cap." }, "spentTodayCents": { "type": "integer", "description": "Gross reservations since 00:00 UTC, including refunds." }, "createdAt": { "type": "integer", "description": "Unix milliseconds." }, "expiresAt": { "type": "integer", "description": "Unix milliseconds." }, "lastUsedAt": { "type": [ "integer", "null" ], "description": "Unix milliseconds or null." }, "revokedAt": { "type": [ "integer", "null" ], "description": "Unix milliseconds or null." } }, "required": [ "id", "name", "prefix", "scopes", "dailyLimitCents", "spentTodayCents", "createdAt", "expiresAt", "lastUsedAt", "revokedAt" ], "additionalProperties": false }, "Account": { "type": "object", "properties": { "id": { "type": "string", "description": "SMSLovers UUID.", "format": "uuid" }, "username": { "type": "string", "description": "Account username." }, "currency": { "type": "string", "const": "EUR" }, "balanceCents": { "type": "integer", "description": "Available balance." }, "key": { "$ref": "#/components/schemas/Key" } }, "required": [ "id", "username", "currency", "balanceCents", "key" ], "additionalProperties": false }, "Ledger": { "type": "object", "properties": { "id": { "type": "integer", "description": "Ledger entry ID." }, "deltaCents": { "type": "integer", "description": "Signed balance change." }, "balanceAfterCents": { "type": "integer", "description": "Resulting balance." }, "description": { "type": "string", "description": "Entry description." }, "createdAt": { "type": "integer", "description": "Unix milliseconds." } }, "required": [ "id", "deltaCents", "balanceAfterCents", "description", "createdAt" ], "additionalProperties": false }, "QuoteOption": { "type": "object", "properties": { "id": { "type": "string", "description": "SMSLovers UUID.", "format": "uuid" }, "priceCents": { "type": "integer", "description": "Exact API charge in EUR cents." }, "retailPriceCents": { "type": "integer", "description": "Normal price for this choice." }, "discountPercent": { "type": "integer", "description": "Whole percent saving, rounded down; 25–50." }, "resendPreferred": { "type": "boolean" }, "label": { "type": "string", "enum": [ "Standard", "Resend" ] }, "expiresAt": { "type": "integer", "description": "Unix milliseconds." } }, "required": [ "id", "priceCents", "retailPriceCents", "discountPercent", "resendPreferred", "label", "expiresAt" ], "additionalProperties": false }, "Quote": { "type": "object", "required": [ "available", "service", "country" ], "properties": { "available": { "type": "boolean" }, "service": { "type": "string", "description": "Service name." }, "serviceId": { "type": "integer", "description": "Service ID." }, "country": { "type": "string", "description": "Country key." }, "id": { "type": "string", "description": "SMSLovers UUID.", "format": "uuid" }, "priceCents": { "type": "integer", "description": "First option price in EUR cents." }, "retailPriceCents": { "type": "integer", "description": "Normal price." }, "discountPercent": { "type": "integer", "description": "Saving." }, "resendPreferred": { "type": "boolean" }, "label": { "type": "string", "description": "First option label." }, "expiresAt": { "type": "integer", "description": "Unix milliseconds." }, "options": { "type": "array", "items": { "$ref": "#/components/schemas/QuoteOption" } }, "resendPoolAvailable": { "type": "boolean" }, "notice": { "type": "string", "description": "Unavailable rate/stock explanation." }, "resendSelectionNotice": { "type": "string", "description": "Resend availability information." } } }, "Order": { "type": "object", "required": [ "id", "service", "country", "priceCents", "status" ], "properties": { "id": { "type": "string", "description": "SMSLovers UUID.", "format": "uuid" }, "serviceId": { "type": "integer", "description": "Service ID." }, "service": { "type": "string", "description": "Service name." }, "country": { "type": "string", "description": "Country key." }, "priceCents": { "type": "integer", "description": "Exact amount charged; refunds use this amount." }, "billingSource": { "type": "string", "enum": [ "api", "retail" ] }, "retailPriceCents": { "type": "integer", "description": "Normal price, present for API orders." }, "discountPercent": { "type": "integer", "description": "Applied API saving." }, "phone": { "type": [ "string", "null" ], "description": "Full international number." }, "callingCode": { "type": [ "string", "null" ], "description": "Country prefix, including +." }, "nationalNumber": { "type": [ "string", "null" ], "description": "Number without calling prefix." }, "status": { "type": "string", "enum": [ "purchasing", "waiting", "received", "cancel_requested", "cancelled", "refunded", "unavailable", "reconciling", "resending", "resend_review" ] }, "code": { "type": [ "string", "null" ], "description": "Latest code. Preserve leading zeroes." }, "message": { "type": [ "string", "null" ], "description": "Full latest SMS text." }, "createdAt": { "type": "integer", "description": "Unix milliseconds." }, "expiresAt": { "type": [ "integer", "null" ], "description": "Unix milliseconds or null." }, "notice": { "type": [ "string", "null" ], "description": "Review information." }, "resendRequestable": { "type": "boolean" }, "resendAvailable": { "type": "boolean" }, "resendNotice": { "type": [ "string", "null" ], "description": "Resend information." }, "resendPreferred": { "type": "boolean" }, "resendCheckable": { "type": "boolean" }, "activeResend": { "type": [ "object", "null" ], "description": "id, status, createdAt and expiresAt of a pending resend." }, "messages": { "type": "array", "items": { "type": "object", "description": "SMS history entries: optional id, code, message, receivedAt (Unix milliseconds) and resend. Codes are strings." } } } }, "Error": { "type": "object", "properties": { "error": { "type": "object", "properties": { "code": { "type": "string", "description": "Stable error category." }, "message": { "type": "string", "description": "Human-readable detail." }, "retryAfterSeconds": { "type": "integer", "description": "Only present for rate limits." } }, "required": [ "code", "message" ], "additionalProperties": false }, "requestId": { "type": "string", "description": "Provide this to support." } }, "required": [ "error", "requestId" ], "additionalProperties": false } } ## Signed webhooks Balance: GET /api/v1/account with account:read. Configure receivers at https://smslovers.com/dashboard/?tab=webhooks or POST /api/v1/webhooks. Subscribe through the API to balance.low, sms.received, sms.ended, sms.review and deposit.credited. deposit.credited requires account:read. Ticket reply notifications are available through dashboard-managed webhooks only. Five HTTPS endpoints per account. Low-balance threshold defaults to 500 EUR cents; alerts fire once per downward crossing and rearm after a refill. An already-low balance queues one initial notification. Headers: X-SMSLovers-Event, X-SMSLovers-Delivery-Id, X-SMSLovers-Timestamp (Unix seconds), X-SMSLovers-Signature (v1=<64 lowercase hex>). Compute HMAC-SHA256 with the entire whsec_... signing-secret string over timestamp + '.' + exact raw body bytes. Constant-time compare; reject timestamps outside 300 seconds. Only then parse JSON. Body envelope: {id,type,accountId,createdAt,data}. Atomically deduplicate the event id, persist and return 2xx quickly; process asynchronously. Delivery IDs and payloads stay stable on retries, timestamps/signatures refresh. At-least-once delivery, no ordering guarantee. Eight attempts with exponential backoff; numeric Retry-After may extend to 24h; no redirects; HTTP410 pauses the receiver. Logs retain metadata for 30 days; private bodies/secrets are not listed. Manual retry is available. URLs must be public HTTPS domains on port443; DNS/private-network/redirect/rebinding protections apply. Private SMS fields are encrypted in the outgoing queue at rest. Signing secrets are shown once on creation/rotation. URL is immutable; create a new endpoint to change it. Key revocation does not disable independently managed receivers. Password reset/change, suspension and account merging pause receivers. Read the full public guide for event payload details: https://smslovers.com/developers/#webhooks Receiver example: // Node.js 22+. Put behind your public HTTPS reverse proxy on port 443. import http from 'node:http'; import { createHmac, timingSafeEqual } from 'node:crypto'; const secret = process.env.SMSLOVERS_WEBHOOK_SECRET; // Full whsec_... string. if (!secret) throw new Error('Set the signing secret privately'); const server = http.createServer(async (req, res) => { if (req.method !== 'POST' || req.url !== '/webhooks/smslovers') { res.writeHead(404).end(); return; } const timestamp = String(req.headers['x-smslovers-timestamp'] || ''); const signature = String(req.headers['x-smslovers-signature'] || ''); if (!/^\d{10}$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300 || !/^v1=[a-f0-9]{64}$/.test(signature)) { res.writeHead(401).end(); return; } try { let size = 0; const chunks = []; for await (const chunk of req) { size += chunk.length; if (size > 32768) { res.writeHead(413).end(); return; } chunks.push(chunk); } const raw = Buffer.concat(chunks); const expected = createHmac('sha256', secret) .update(timestamp + '.').update(raw).digest(); const supplied = Buffer.from(signature.slice(3), 'hex'); if (!timingSafeEqual(expected, supplied)) { res.writeHead(401).end(); return; } const event = JSON.parse(raw.toString('utf8')); // REQUIRED: atomically persist event.id with a unique constraint and // enqueue the work in your database. If already stored, return 204. // await persistAndEnqueueOnce(event.id, event); // Replace the line above with your own durable queue before production. console.log('Verified event type:', event.type); // Never log private codes. res.writeHead(204).end(); // Acknowledge after durable storage succeeds. } catch { res.writeHead(500).end(); // SMSLovers will retry with the same event ID. } }); server.requestTimeout = 15000; server.listen(3000, '127.0.0.1'); ## Build with an AI agent Build an integration with the SMSLovers Public API. First read https://smslovers.com/developers/ and its OpenAPI 3.1 specification at https://smslovers.com/api-openapi.json (plain-text reference: https://smslovers.com/api-docs.md). Use https://smslovers.com/api/v1 with Authorization: Bearer , loaded only from a server environment variable. Never put the key in client code, URLs, logs, commits or AI chat. Ask me to create a scoped key in https://smslovers.com/dashboard/?tab=api and enter it privately in my deployment environment. Discover service IDs and exact country keys; POST /quotes, let the user choose a returned option, and POST /orders with its quoteId and a durable unique Idempotency-Key. Reuse the same key and identical body after a timeout; never blindly repurchase a reconciling order. Poll GET /orders/{id} every 5–10 seconds, honor 429 Retry-After, and preserve leading zeroes in SMS codes. Use the docs for cancellation, free resends, balance/ledger and signed webhooks. Add balance and contact support through the SMSLovers website or Discord bot. There are no deposit, promo or support-ticket API endpoints. For webhooks, verify X-SMSLovers-Signature (v1=HMAC-SHA256 of timestamp + a dot + the exact raw JSON body, using the whsec_ secret as the key). Reject timestamps outside 5 minutes, deduplicate event IDs and acknowledge promptly with 2xx. See the public webhook guide. All EUR amounts are integer cents; timestamps are Unix milliseconds. API rates save 25–50% depending on eligible availability, with the quoted price final. Website/Discord purchases use their regular prices. Start with mocked tests and read-only calls; do not place a paid order until I explicitly approve it. Implement server-side ownership, input validation, secure secret storage, idempotency persistence, timeouts and backoff. Do not invent endpoints or request higher permissions than needed. ## Custom development Discord bot rebrands: €50 each. Paid custom solutions and additional features by quote. Request help at https://smslovers.com/support/.