Skip to content
Last updated

Webhook configuration

Each SMS message can be linked to a specific webhook configuration to ensure notifications are sent to the designated endpoint.

Example in the send methods:

{
  "webhook": {
    "url": "https://example.com/callback",
    "authentication": {
        "type": "NONE"
    }
  }
}

The webhook settings include the following parameters.

  • url: The destination URL where webhook notifications will be sent.
    • Must be a valid, publicly accessible URL
    • HTTPS is required
  • authentication: The authentication method used when calling your endpoint.
    • NONE: No authentication is required. The webhook request will be sent without any credentials.
    • JWS: Namirial signs each outgoing callback with an RSA private key (RS256) and attaches the signature as an X-JWS-Signature header. This allows you to verify the authenticity and integrity of every notification. See Verify JWS signatures below.

The webhook configuration is defined per message, inline in the send request. There is no global reusable configuration.

For details on notification payloads, retry behavior, and status transitions, see Understanding webhook.

Verify JWS signatures

When type is JWS, Namirial signs each callback and attaches a compact JWS token (RFC 7515, RS256) to the x-jws-signature request header. The request body is sent in plain text — JWS guarantees authenticity and integrity.

The x-jws-signature header contains a compact JWS token with three base64url-encoded parts separated by .:

x-jws-signature: <header>.<payload>.<signature>

Example:

eyJraWQiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiLCJhbGciOiJSUzI1NiJ9
.
eyJhdWQiOiJodHRwczovL2V4YW1wbGUuY29tL2NhbGxiYWNrIiwic3ViIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwIiwiYm9keV9zaGEyNTYiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE9IiwiaXNzIjoiaHR0cHM6Ly9zbXMtYXBpc3RhdHVzLm5hbWlyaWFsdHNwLmNvbS9hcGkiLCJleHAiOjE3MDAwMDAwMDAsImlhdCI6MTcwMDAwMDAwMCwianRpIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwIn0
.
SIGNATURE

Decoded:

  • Header: {"kid":"00000000-0000-0000-0000-000000000000","alg":"RS256"}
  • Payload: {"aud":"https://example.com/callback","sub":"00000000-0000-0000-0000-000000000000","body_sha256":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","iss":"https://sms-apistatus.namirialtsp.com/api","exp":1700000000,"iat":1700000000,"jti":"00000000-0000-0000-0000-000000000000"}
  • Signature: base64url-encoded RSA signature

To verify a callback, follow these steps.

Step 1 — Retrieve the key ID

Decode the JWS header (the first part of the token, before the first .) from base64url to JSON.

{
  "alg": "RS256",
  "kid": "00000000-0000-0000-0000-000000000000"
}
  • alg: the signing algorithm — always RS256 (RSASSA-PKCS1-v1_5 using SHA-256, as described in RFC7518 §3.1)
  • kid: the key ID — identifies which public key was used to sign the request

Step 2 — Retrieve the public key

Fetch the JWKS endpoint and find the key matching the kid from step 1:

GET https://sms-apistatus.namirialtsp.com/api/.well-known/jwks.json

Step 3 — Inspect the payload

The signed input is the JWS token without the signature — take the x-jws-signature value and remove everything after the last . (the signature part). What remains, <header>.<payload>, is the exact string that was signed.

The payload, once base64url-decoded, contains the following claims:

{
  "iss": "https://sms-apistatus.namirialtsp.com/api",
  "aud": "https://example.com/callback",
  "sub": "00000000-0000-0000-0000-000000000000",
  "iat": 1700000000,
  "exp": 1700000300,
  "jti": "00000000-0000-0000-0000-000000000001",
  "body_sha256": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
}
ClaimDescription
issIssuer — identifies Namirial (e.g. https://sms-apistatus.namirialtsp.com/api)
audAudience — your webhook URL
subSubject — the messageId of the SMS
iatIssued-at timestamp (Unix)
expExpiry timestamp — token is valid for 5 minutes (iat + 300)
jtiUnique token ID — use for replay protection
body_sha256Base64url-encoded SHA-256 hash of the raw request body

Step 4 — Verify the signature

Using the algorithm specified in alg (step 1) and the public key retrieved in step 2, verify that the signature (the third part of the x-jws-signature token) is a valid signature of the string <header>.<payload> — i.e. the token with the signature part removed.

If the verification fails, the callback must be rejected.

token   = request.headers["x-jws-signature"]
parts   = token.split(".")

header    = base64url_decode(parts[0])
payload   = base64url_decode(parts[1])
signature = base64url_decode(parts[2])

signed_input = parts[0] + "." + parts[1]
public_key   = jwks_get_key(header["kid"])

assert rsa_sha256_verify(public_key, signed_input, signature)

Verify body integrity

The JWS payload includes a body_sha256 claim containing the base64url-encoded SHA-256 hash of the raw request body. After verifying the signature, compute the SHA-256 hash of the received body and compare it with body_sha256 to confirm the payload has not been altered.

Example

x-jws-signature header:

eyJraWQiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJodHRwczovL2V4YW1wbGUuY29tL2NhbGxiYWNrIiwic3ViIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwIiwiYm9keV9zaGEyNTYiOiJBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE9IiwiaXNzIjoiaHR0cHM6Ly9zbXMtYXBpc3RhdHVzLm5hbWlyaWFsdHNwLmNvbS9hcGkiLCJleHAiOjE3MDAwMDAwMDAsImlhdCI6MTcwMDAwMDAwMCwianRpIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwIn0.SIGNATURE

Request body:

{
  "id": "00000000-0000-0000-0000-000000000001",
  "webhookConfigurationId": "00000000-0000-0000-0000-000000000002",
  "type": "STATUS_UPDATE",
  "createdAt": "2024-01-01T00:00:00Z",
  "message": {
    "id": "00000000-0000-0000-0000-000000000000",
    "reference": {},
    "channel": "SMS",
    "status": "DELIVERED",
    "providerAcceptanceAt": "2024-01-01T00:00:00Z",
    "statusChangedAt": "2024-01-01T00:00:01Z",
    "provider": {
      "name": "PROVIDER1",
      "id": "00000000-0000-0000-0000-000000000003",
      "status": "DELIVERED",
      "code": "0"
    },
    "sms": { "segments": 1 }
  }
}