Skip to content
Developers

Pingy API documentation

Send WhatsApp messages and verification codes from your own system through a RESTful API. Read how the integration works before you create an account.

Protocol
REST over HTTPS, JSON in and out
Authentication
Bearer API key from your dashboard
Environments
Sandbox for testing, Live for real sending
Base URL
Shown in your dashboard for each environment

How it works

Pingy is a RESTful API over HTTPS. Your server sends a JSON request with an API key, and Pingy delivers the message through the official WhatsApp Business Platform from the shared Pingy number or from your own connected number. Calls over plain HTTP and calls without a key fail.

Every message uses a template that Meta has approved. You write the template in your dashboard, Pingy submits it to Meta for review, and once it is approved you use its template_id in your requests. Your request chooses the template; your dashboard chooses the sender number, so the same request works with the shared number and with your own number.

Verification codes and utility messages work with both numbers. Marketing messages need your own connected number.

EndpointWhat it doesScope
POST /customer/v1/send/serviceSend a utility or marketing messagemessage.send
POST /customer/v1/otp/requestSend a verification codeotp.request
POST /customer/v1/otp/verifyCheck the code your customer enteredotp.verify
POST /customer/v1/messages/statusRead the current state of a messagemessages.read, or otp.read for a verification code
POST /customer/v1/messages/cancelCancel a message that is still queuedmessage.send, or otp.request for a verification code

All endpoints use POST with Content-Type: application/json. A 202 response with the state accepted means Pingy accepted the request. Delivery is reported later through the status endpoint, and your balance is charged only for delivered messages.

Before your first request

  1. Create your account and workspace. Your workspace_id is shown in the dashboard under Integrations.
  2. Write a template and wait for Meta's approval. Copy its template_id from the Templates page once it is approved for the number you use.
  3. Create an API key under Integrations. Start with a Sandbox key to test, then create a Live key. Give each key only the scopes the integration needs. The key is shown once; store it on your server.
  4. Send your first request to the Sandbox base URL, then switch the base URL and the key to Live. The base URL for each environment is shown in your dashboard.

The examples below read the base URL and the key from the environment variables PINGY_API_BASE_URL and PINGY_SECRET_KEY. Replace the placeholder IDs and sample values with your own.

Authentication and API keys

Send your key in the Authorization header as a Bearer token. Every request also needs a JSON content type:

Required headers
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

Keys start with ak1_ and carry many privileges. Requests are server to server: browser cookies and requests coming from a web page are rejected, so never embed a key in a mobile app, in front-end code or in source control. Keep it in your server's secrets store or environment variables.

KeyBase URLEffect
Sandbox keySandbox base URLSimulated results. Nothing is sent to WhatsApp and your balance is not touched.
Live keyLive base URLReal sending from your prepaid balance.

A Live key is rejected by the Sandbox, and a Sandbox key is rejected by Live. Each key carries the scopes you chose when you created it:

ScopeAllows
message.sendSend a utility or marketing message, and cancel one that is still queued
messages.readRead the state of a message
otp.requestSend a verification code, and cancel one that is still queued
otp.verifyCheck the code your customer entered
otp.readRead the state of a verification-code message

Create a separate key for each integration and give it only the scopes it needs. Revoking a key stops its future requests at once; it does not retract a message that was already accepted. Renaming a key changes its label only. All Live keys of a workspace share the same balance.

Send a message

POST /customer/v1/send/service sends a utility or marketing message. The approved template decides the category; the request format is the same for both.

Body parameters

workspace_idstringrequired
Your workspace ID, shown in the dashboard under Integrations.
template_idstringrequired
The ID of an approved template, copied from the dashboard.
recipientstringrequired
The customer's phone number in international format with a plus sign, for example +12025550199. Pattern: ^\+[1-9][0-9]{7,14}$.
idempotency_keystringrequired
A unique value you generate for this request, for example your order ID. Sending it again with the same data returns the original attempt instead of a new message.
variablesstring[]
The template values in the order of the template parameters. Omit it for a template without parameters; a template with parameters needs every value.
template_revisioninteger
A positive number that pins a template revision. Omit it to use the current approved revision.

Templates with media or special layouts take extra optional fields: header_values, media_url, media_type, document_filename, location, carousel_values, commerce_values and flow_data_json. Include only the fields your approved template needs. Unknown fields, duplicate fields and null values are rejected.

POST/customer/v1/send/service
curl --request POST "$PINGY_API_BASE_URL/customer/v1/send/service" \
  --header "Authorization: Bearer $PINGY_SECRET_KEY" \
  --header "Content-Type: application/json" \
  --data '{
  "workspace_id": "YOUR_WORKSPACE_ID",
  "template_id": "YOUR_TEMPLATE_ID",
  "recipient": "+12025550199",
  "idempotency_key": "order-1048",
  "variables": [
    "Maya",
    "AC-1048"
  ]
}'

Response

attempt_idstring
The message identifier. Keep it to read the state and to find the message in Message logs.
statestring
accepted on a new request. See Check message status for every state.
replayedboolean
true when Pingy had already accepted the same idempotency_key with the same data and returned the original attempt instead of sending again.

Send a verification code

A verification code takes two calls. POST /customer/v1/otp/request generates the code on the server and sends it with an approved Authentication template. POST /customer/v1/otp/verify checks the code your customer typed. The code itself is never returned to you.

Request a code

Body parameters

workspace_idstringrequired
Your workspace ID, shown in the dashboard under Integrations.
template_idstringrequired
The ID of an approved Authentication template.
recipientstringrequired
The customer's phone number in international format with a plus sign, for example +12025550199. Pattern: ^\+[1-9][0-9]{7,14}$.
idempotency_keystringrequired
A unique value you generate for this request, for example your order ID. Sending it again with the same data returns the original attempt instead of a new message.
purposestringrequired
What the code is for, for example sign_in or password_reset. Send the same value when you verify.
transactionstringrequired
A value that ties the code to this operation, encoded as unpadded base64url, up to 256 bytes before encoding. Send the same value when you verify.
POST/customer/v1/otp/request
curl --request POST "$PINGY_API_BASE_URL/customer/v1/otp/request" \
  --header "Authorization: Bearer $PINGY_SECRET_KEY" \
  --header "Content-Type: application/json" \
  --data '{
  "workspace_id": "YOUR_WORKSPACE_ID",
  "template_id": "YOUR_AUTHENTICATION_TEMPLATE_ID",
  "recipient": "+12025550199",
  "idempotency_key": "sign-in-7f3a",
  "purpose": "sign_in",
  "transaction": "c2lnbl9pbg"
}'

Response

verification_idstring
The challenge identifier. Keep it for the verify call.
attempt_idstring
The identifier of the WhatsApp message that carries the code.
challenge_statestring
active while the code can be verified; later consumed, expired, locked, superseded or cancelled.
delivery_statestring
The message state, accepted on a new request.
expires_attimestamp
When the code stops working. created_at is returned as well.
replayedboolean
true when the same request was already accepted.

Verify the code

When the customer enters the code, call verify with the same recipient, purpose and transaction:

Body parameters

workspace_idstringrequired
Your workspace ID, shown in the dashboard under Integrations.
verification_idstringrequired
The value returned by the request call.
recipientstringrequired
The customer's phone number in international format with a plus sign, for example +12025550199. Pattern: ^\+[1-9][0-9]{7,14}$.
purposestringrequired
The same purpose you sent when requesting the code.
transactionstringrequired
The same transaction value you sent when requesting the code.
codestringrequired
The code your customer typed.
POST/customer/v1/otp/verify
curl --request POST "$PINGY_API_BASE_URL/customer/v1/otp/verify" \
  --header "Authorization: Bearer $PINGY_SECRET_KEY" \
  --header "Content-Type: application/json" \
  --data '{
  "workspace_id": "YOUR_WORKSPACE_ID",
  "verification_id": "VERIFICATION_ID_FROM_REQUEST",
  "recipient": "+12025550199",
  "purpose": "sign_in",
  "transaction": "c2lnbl9pbg",
  "code": "CODE_ENTERED_BY_CUSTOMER"
}'

A code works once. Its expiry and the number of allowed guesses come from the verification policy of your workspace. verified is false for a wrong, expired or already-used code. If the customer needs a new code, start a new request with a new idempotency_key. Verification codes must be enabled for your workspace; the dashboard shows whether they are.

Check message status

POST /customer/v1/messages/status returns the current state of a message. Use the same key that created it, with the messages.read scope, or otp.read for a verification code. Reading the state sends nothing.

Body parameters

workspace_idstringrequired
Your workspace ID, shown in the dashboard under Integrations.
attempt_idstringrequired
The identifier returned when the message was accepted.
POST/customer/v1/messages/status
curl --request POST "$PINGY_API_BASE_URL/customer/v1/messages/status" \
  --header "Authorization: Bearer $PINGY_SECRET_KEY" \
  --header "Content-Type: application/json" \
  --data '{
  "workspace_id": "YOUR_WORKSPACE_ID",
  "attempt_id": "ATTEMPT_ID_FROM_RESPONSE"
}'
StateMeaning
acceptedPingy accepted the request and is sending it.
queuedWaiting to be sent. It can still be cancelled.
dispatchingBeing handed to WhatsApp.
deliveredWhatsApp confirmed delivery. This is the only state that is charged.
failedNot delivered. Nothing is charged.
unknown, reconciliation_requiredThe outcome is not confirmed yet. Read the state again later or check Message logs.
cancelledCancelled before it was sent.

Cancel a queued message

POST /customer/v1/messages/cancel takes the same workspace_id and attempt_id and cancels a message that is still queued. A message already handed to WhatsApp cannot be cancelled. Cancelling a verification-code message also cancels its code.

POST/customer/v1/messages/cancel
curl --request POST "$PINGY_API_BASE_URL/customer/v1/messages/cancel" \
  --header "Authorization: Bearer $PINGY_SECRET_KEY" \
  --header "Content-Type: application/json" \
  --data '{
  "workspace_id": "YOUR_WORKSPACE_ID",
  "attempt_id": "ATTEMPT_ID_FROM_RESPONSE"
}'

You can also follow every message in Message logs in your dashboard.

Retries and idempotency

Every send and verification request carries an idempotency_key that you generate. If the connection drops before you receive a response, send exactly the same request again with the same key: Pingy returns the original attempt with replayed: true and does not send a second message. Sending the same key with different data returns 409 conflict. Do not create a new key only because a request timed out.

Idempotency is tied to the workspace and to the exact API key. Rotating the key in the middle of a retry can create a new message.

Sandbox (test environment)

The Sandbox has its own base URL and works with Sandbox keys. It accepts the same endpoints, fields and responses as Live, but sends nothing to WhatsApp and never touches your balance. Build and test your integration against it, then switch the base URL and the key to Live.

RecipientSimulated result
+12025550199 and any other valid numberaccepted, then delivered when you read the state after one second
+12025550198failed
+12025550197queued; you can cancel it
+12025550196unknown; it cannot be cancelled
+12025550195HTTP 429 rate_limited

No number is contacted. The Sandbox verification code is always 000000; it expires after 5 minutes, allows 5 guesses and works once.

Use your own workspace template IDs so your code stays the same in Live. If you have no approved template yet, the Sandbox accepts three fixture templates at revision 1: wt1_99999999999999999999999999999991 (utility, two body values), wt1_99999999999999999999999999999992 (marketing, two body values) and wt1_99999999999999999999999999999993 (authentication). Fixture IDs work only in the Sandbox.

Sandbox records expire after 24 hours. The Sandbox checks the shape of your request and the template values; it does not check media URLs, Meta approval or the connection of your number. Have those ready before you go Live. Your dashboard also includes a Playground that runs these requests from the browser with a Sandbox key.

Errors and limits

A rejected request returns an HTTP error code and a JSON body with one field, status:

Error response
{ "status": "invalid" }
HTTPstatusWhat to do
400invalidThe JSON is malformed, a field is missing or unknown, or a value is out of range. Check the fields, the template revision and the number of variables.
401, 403rejectedThe key is missing, revoked, for the other environment or lacks the scope, or the template is not approved for the selected number.
409conflictThe idempotency_key was already used with different data. Reconcile the original request.
429rate_limitedToo many requests. Wait, then retry the same request.
503unavailablePingy could not complete the request. Keep the same idempotency_key and check the outcome before trying again.
404, 405Unknown path, or a method other than POST.

Limits: a send request body is at most 64 KiB and a verification request 8 KiB. Query strings, unsupported content types, unknown fields, duplicate JSON keys and null values are rejected.

Ready to send your first message?

Create your account, test in the Sandbox at no cost, then go live with your prepaid balance.