Contents

Get started

Authentication and API keys

Every API request carries a secret API key. Keys belong to one account and one environment, carry only the permissions you give them, and can be locked to your servers’ addresses.

Sending the key

Send the secret as a bearer token over HTTPS:

curl
curl "https://api.shipmsg.com/v1/accounts/$SHIPMESSAGES_ACCOUNT_SID/senders" \
  -H "Authorization: Bearer $SHIPMESSAGES_API_KEY"

A missing, wrong or revoked key answers 401 with AUTHENTICATION_ERROR. Keys are secrets: keep them in your server’s environment or secret store, never in a browser, a mobile app or source control.

Sandbox and live keys

PrefixEnvironmentWhat it can reach
sk_test_SandboxThe sandbox sender, test numbers and sandbox data. Never reaches a real phone; never billed.
sk_live_LiveYour connected WhatsApp numbers and real customers. Billed.

The two environments are fully separate: a sandbox key cannot see live messages, contacts or senders, and the other way round. Every resource says which one it belongs to in livemode.

Creating, rotating and revoking keys

Create keys in the console under Developer → API keys. The secret is shown once; ShipMsg keeps only a one-way hash of it, so a lost secret cannot be shown again. Rotate a key to get a replacement secret, and revoke a key the moment it may have leaked. Creating, rotating and revoking keys needs a signed-in user who confirms with their second factor; an API key cannot mint other keys.

Scopes

A key carries a list of scopes of the form resource:action, for example messages:write or templates:read; * grants everything. A call outside the key’s scopes answers 403 AUTHORIZATION_ERROR. Give each integration the narrowest set it needs.

IP allow-lists and expiry

A key can be limited to a list of CIDR ranges (allowedCidrs) and given an expiry date (expiresAt). A request from any other address is refused as if the key were wrong.

Request signing (optional)

For keys created with requireSigning, every request must also be signed with HMAC-SHA256, so a leaked bearer token alone is not enough. The SDKs do this for you when you pass the key SID and secret as signing. To sign by hand, build this string — seven lines joined by \n, no trailing newline:

canonical string
v1
POST
/v1/accounts/AC…/messages          (percent-decoded, then RFC 3986 encoded, no trailing slash)
                                   (query: name=value pairs sorted by name then value, joined by &)
1790372446                         (Unix seconds; must be within 5 minutes of the server clock)
Yx2c…                              (nonce: at least 16 random bytes, base64url, never reused)
e3b0c442…                          (hex SHA-256 of the exact body bytes; of "" when there is none)

and send four headers with the request:

headers
X-ShipMessages-Key: SK…                  # the key's SID
X-ShipMessages-Timestamp: 1790372446
X-ShipMessages-Nonce: Yx2c…
X-ShipMessages-Signature: v1=<hex HMAC-SHA256(secret, canonical string)>

SDK identification

The official SDKs send X-ShipMessages-Client: <language>/<version> (for example node/0.1.0). It is recorded in your request log so you can see which SDK and version made a call; it never affects what the request is allowed to do. Other X-ShipMessages-* headers the API does not know are refused with 400, so a typo in a header name is caught rather than silently ignored.