Skip to content

API reference

Webhooks

Register and manage the endpoints SMSend delivers events to.

List webhook endpoints

GET/public/v1/webhooks

Returns every endpoint registered to the account, newest first, with its current health.

Required scope WEBHOOKS_MANAGE

Request

curl https://api.smsend.net/public/v1/webhooks \
  -H "Authorization: Bearer $SMSEND_API_KEY"
<?php

$webhooks = Http::withToken(getenv('SMSEND_API_KEY'))
    ->get('https://api.smsend.net/public/v1/webhooks')
    ->json('data.webhooks');
const response = await fetch('https://api.smsend.net/public/v1/webhooks', {
  headers: { Authorization: `Bearer ${process.env.SMSEND_API_KEY}` },
})

const { data } = await response.json()
import os
import requests

webhooks = requests.get(
    "https://api.smsend.net/public/v1/webhooks",
    headers={"Authorization": f"Bearer {os.environ['SMSEND_API_KEY']}"},
).json()["data"]["webhooks"]

Responses

200The account's webhook endpoints. Secrets are never included in a read.
{
  "data": {
    "webhooks": [
      {
        "webhook_id": "01K3F8ZZ1122334455667788AA",
        "url": "https://hooks.example.bf/smsend",
        "description": "Production receipts",
        "events": ["MESSAGE_TERMINAL", "BATCH_SETTLED"],
        "status": "ENABLED",
        "consecutive_failures": 0,
        "disabled_reason": null,
        "last_success_at": "2026-08-23T14:05:20+00:00",
        "last_failure_at": null,
        "created_at": "2026-07-02T08:31:44+00:00"
      }
    ]
  }
}

Possible errors

CodeStatusMeaning
UNAUTHORIZED401No key, a malformed key, a wrong secret, a revoked or expired key, or a suspended account. All indistinguishable by design.
INSUFFICIENT_SCOPE403The key is valid but does not carry the scope this endpoint requires. The scope is named in the detail.
RATE_LIMITED429The key's request limit was exceeded. Wait for the interval in Retry-After.
INTERNAL_ERROR500An unexpected failure on SMSend's side. Quote the request id to support.

Create a webhook endpoint

POST/public/v1/webhooks

Registers an endpoint and issues its signing secret. This is the only response that ever contains the secret.

Required scope WEBHOOKS_MANAGE

Body parameters

urlstringRequired

Where to deliver events. Must be HTTPS and must not resolve to a private, loopback or link-local address. A hostname whose DNS has not propagated yet is accepted.

max 2048 characters, must be HTTPS, no private or loopback address

descriptionstringOptional

A label for your own use, shown in the console.

max 255 characters

eventsarrayOptional

Which events to deliver. Duplicates are removed; an empty array falls back to the default.

default ["MESSAGE_TERMINAL", "BATCH_SETTLED"]

Request

curl -X POST https://api.smsend.net/public/v1/webhooks \
  -H "Authorization: Bearer $SMSEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.example.bf/smsend",
    "description": "Production receipts",
    "events": ["MESSAGE_TERMINAL", "BATCH_SETTLED", "BATCH_ACCEPTED"]
  }'
<?php

$webhook = Http::withToken(getenv('SMSEND_API_KEY'))
    ->post('https://api.smsend.net/public/v1/webhooks', [
        'url' => 'https://hooks.example.bf/smsend',
        'description' => 'Production receipts',
        'events' => ['MESSAGE_TERMINAL', 'BATCH_SETTLED', 'BATCH_ACCEPTED'],
    ])
    ->json('data');

// The only response that ever carries the secret. Store it now.
storeWebhookSecret($webhook['webhook_id'], $webhook['secret']);
const response = await fetch('https://api.smsend.net/public/v1/webhooks', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SMSEND_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    url: 'https://hooks.example.bf/smsend',
    description: 'Production receipts',
    events: ['MESSAGE_TERMINAL', 'BATCH_SETTLED', 'BATCH_ACCEPTED'],
  }),
})

const { data } = await response.json()

// The only response that ever carries the secret. Store it now.
storeWebhookSecret(data.webhook_id, data.secret)
import os
import requests

webhook = requests.post(
    "https://api.smsend.net/public/v1/webhooks",
    headers={"Authorization": f"Bearer {os.environ['SMSEND_API_KEY']}"},
    json={
        "url": "https://hooks.example.bf/smsend",
        "description": "Production receipts",
        "events": ["MESSAGE_TERMINAL", "BATCH_SETTLED", "BATCH_ACCEPTED"],
    },
).json()["data"]

# The only response that ever carries the secret. Store it now.
store_webhook_secret(webhook["webhook_id"], webhook["secret"])

Responses

201The endpoint, including its secret. Store the secret now — it cannot be retrieved again.
{
  "data": {
    "webhook_id": "01K3F8ZZ1122334455667788AA",
    "url": "https://hooks.example.bf/smsend",
    "description": "Production receipts",
    "events": ["MESSAGE_TERMINAL", "BATCH_SETTLED", "BATCH_ACCEPTED"],
    "status": "ENABLED",
    "consecutive_failures": 0,
    "disabled_reason": null,
    "last_success_at": null,
    "last_failure_at": null,
    "created_at": "2026-08-23T14:22:07+00:00",
    "secret": "whsec_3f8a1c9e0b7d452a6e18cf30b95d7e421ac6f08b3d92e574a10c8fb26d94e753"
  }
}

Possible errors

CodeStatusMeaning
VALIDATION_FAILED422The request was malformed: a missing or invalid parameter, a bad idempotency key length, a rejected webhook URL, or an unsupported method or content type.
UNAUTHORIZED401No key, a malformed key, a wrong secret, a revoked or expired key, or a suspended account. All indistinguishable by design.
INSUFFICIENT_SCOPE403The key is valid but does not carry the scope this endpoint requires. The scope is named in the detail.
RATE_LIMITED429The key's request limit was exceeded. Wait for the interval in Retry-After.
INTERNAL_ERROR500An unexpected failure on SMSend's side. Quote the request id to support.

Rotate the signing secret

POST/public/v1/webhooks/{webhook_id}/rotate

Issues a new secret and keeps the previous one valid for 24 hours. Both verify during the window while SMSend signs with the new one, so you can deploy without dropping deliveries.

Required scope WEBHOOKS_MANAGE

Path parameters

webhook_idstringRequired

The endpoint identifier, returned when it was created.

26-character ULID

Request

curl -X POST https://api.smsend.net/public/v1/webhooks/01K3F8ZZ1122334455667788AA/rotate \
  -H "Authorization: Bearer $SMSEND_API_KEY"
<?php

$webhook = Http::withToken(getenv('SMSEND_API_KEY'))
    ->post('https://api.smsend.net/public/v1/webhooks/01K3F8ZZ1122334455667788AA/rotate')
    ->json('data');
const response = await fetch(
  'https://api.smsend.net/public/v1/webhooks/01K3F8ZZ1122334455667788AA/rotate',
  { method: 'POST', headers: { Authorization: `Bearer ${process.env.SMSEND_API_KEY}` } },
)

const { data } = await response.json()
import os
import requests

webhook = requests.post(
    "https://api.smsend.net/public/v1/webhooks/01K3F8ZZ1122334455667788AA/rotate",
    headers={"Authorization": f"Bearer {os.environ['SMSEND_API_KEY']}"},
).json()["data"]

Responses

200The endpoint with its new secret, and the moment the old one stops working.
{
  "data": {
    "webhook_id": "01K3F8ZZ1122334455667788AA",
    "url": "https://hooks.example.bf/smsend",
    "description": "Production receipts",
    "events": ["MESSAGE_TERMINAL", "BATCH_SETTLED"],
    "status": "ENABLED",
    "consecutive_failures": 0,
    "disabled_reason": null,
    "last_success_at": "2026-08-23T14:05:20+00:00",
    "last_failure_at": null,
    "created_at": "2026-07-02T08:31:44+00:00",
    "secret": "whsec_a71b40e2c6d9385f1e02ba7c48d3906fe25b1837cd04a96e2f7b58c103da4e61",
    "previous_secret_expires_at": "2026-08-24T14:22:07+00:00"
  }
}

Possible errors

CodeStatusMeaning
NOT_FOUND404No such resource — including one that belongs to another account, which is deliberately indistinguishable from one that never existed.
UNAUTHORIZED401No key, a malformed key, a wrong secret, a revoked or expired key, or a suspended account. All indistinguishable by design.
INSUFFICIENT_SCOPE403The key is valid but does not carry the scope this endpoint requires. The scope is named in the detail.
RATE_LIMITED429The key's request limit was exceeded. Wait for the interval in Retry-After.
INTERNAL_ERROR500An unexpected failure on SMSend's side. Quote the request id to support.

Enable an endpoint

POST/public/v1/webhooks/{webhook_id}/enable

Re-enables a disabled endpoint and resets its failure counter, so a recovered endpoint is not switched off again by the first failure after it comes back.

Required scope WEBHOOKS_MANAGE

Path parameters

webhook_idstringRequired

The endpoint identifier, returned when it was created.

26-character ULID

Request

curl -X POST https://api.smsend.net/public/v1/webhooks/01K3F8ZZ1122334455667788AA/enable \
  -H "Authorization: Bearer $SMSEND_API_KEY"
<?php

$webhook = Http::withToken(getenv('SMSEND_API_KEY'))
    ->post('https://api.smsend.net/public/v1/webhooks/01K3F8ZZ1122334455667788AA/enable')
    ->json('data');
const response = await fetch(
  'https://api.smsend.net/public/v1/webhooks/01K3F8ZZ1122334455667788AA/enable',
  { method: 'POST', headers: { Authorization: `Bearer ${process.env.SMSEND_API_KEY}` } },
)

const { data } = await response.json()
import os
import requests

webhook = requests.post(
    "https://api.smsend.net/public/v1/webhooks/01K3F8ZZ1122334455667788AA/enable",
    headers={"Authorization": f"Bearer {os.environ['SMSEND_API_KEY']}"},
).json()["data"]

Responses

200The re-enabled endpoint.
{
  "data": {
    "webhook_id": "01K3F8ZZ1122334455667788AA",
    "url": "https://hooks.example.bf/smsend",
    "description": "Production receipts",
    "events": ["MESSAGE_TERMINAL", "BATCH_SETTLED"],
    "status": "ENABLED",
    "consecutive_failures": 0,
    "disabled_reason": null,
    "last_success_at": "2026-08-23T14:05:20+00:00",
    "last_failure_at": "2026-08-23T18:40:02+00:00",
    "created_at": "2026-07-02T08:31:44+00:00"
  }
}

Possible errors

CodeStatusMeaning
NOT_FOUND404No such resource — including one that belongs to another account, which is deliberately indistinguishable from one that never existed.
UNAUTHORIZED401No key, a malformed key, a wrong secret, a revoked or expired key, or a suspended account. All indistinguishable by design.
INSUFFICIENT_SCOPE403The key is valid but does not carry the scope this endpoint requires. The scope is named in the detail.
RATE_LIMITED429The key's request limit was exceeded. Wait for the interval in Retry-After.
INTERNAL_ERROR500An unexpected failure on SMSend's side. Quote the request id to support.

Disable an endpoint

POST/public/v1/webhooks/{webhook_id}/disable

Stops delivery to an endpoint without deleting it. Events created while it is disabled are not queued for later — recover them from the messages feed.

Required scope WEBHOOKS_MANAGE

Path parameters

webhook_idstringRequired

The endpoint identifier, returned when it was created.

26-character ULID

Request

curl -X POST https://api.smsend.net/public/v1/webhooks/01K3F8ZZ1122334455667788AA/disable \
  -H "Authorization: Bearer $SMSEND_API_KEY"
<?php

$webhook = Http::withToken(getenv('SMSEND_API_KEY'))
    ->post('https://api.smsend.net/public/v1/webhooks/01K3F8ZZ1122334455667788AA/disable')
    ->json('data');
const response = await fetch(
  'https://api.smsend.net/public/v1/webhooks/01K3F8ZZ1122334455667788AA/disable',
  { method: 'POST', headers: { Authorization: `Bearer ${process.env.SMSEND_API_KEY}` } },
)

const { data } = await response.json()
import os
import requests

webhook = requests.post(
    "https://api.smsend.net/public/v1/webhooks/01K3F8ZZ1122334455667788AA/disable",
    headers={"Authorization": f"Bearer {os.environ['SMSEND_API_KEY']}"},
).json()["data"]

Responses

200The disabled endpoint.
{
  "data": {
    "webhook_id": "01K3F8ZZ1122334455667788AA",
    "url": "https://hooks.example.bf/smsend",
    "description": "Production receipts",
    "events": ["MESSAGE_TERMINAL", "BATCH_SETTLED"],
    "status": "DISABLED_BY_USER",
    "consecutive_failures": 0,
    "disabled_reason": "Disabled by the account.",
    "last_success_at": "2026-08-23T14:05:20+00:00",
    "last_failure_at": null,
    "created_at": "2026-07-02T08:31:44+00:00"
  }
}

Possible errors

CodeStatusMeaning
NOT_FOUND404No such resource — including one that belongs to another account, which is deliberately indistinguishable from one that never existed.
UNAUTHORIZED401No key, a malformed key, a wrong secret, a revoked or expired key, or a suspended account. All indistinguishable by design.
INSUFFICIENT_SCOPE403The key is valid but does not carry the scope this endpoint requires. The scope is named in the detail.
RATE_LIMITED429The key's request limit was exceeded. Wait for the interval in Retry-After.
INTERNAL_ERROR500An unexpected failure on SMSend's side. Quote the request id to support.

Endpoint statuses

The two disabled states are kept apart on purpose: one is a decision you made, the other is a failure you need to investigate, and they call for different responses.

ValueMeaning
ENABLED
Receiving deliveries.
DISABLED_BY_USER
Paused by the account. Re-enable it when you are ready.
DISABLED_AFTER_FAILURES
Switched off by SMSend after 20 consecutive failures. Fix the endpoint, then re-enable it explicitly.

Not on this surface

Changing an endpoint's URL, its event subscriptions or its description, and deleting an endpoint entirely, are done in the customer console rather than through the API. Creating and revoking API keys is likewise console-only — a published key-minting endpoint would turn a single leaked key into permanent access.