Voicegram

Create a webhook subscription

Creates a webhook subscription that fires the `voicegram.completed` event to the supplied URL on every matching completed voicegram for the account.

On this page

Creates a webhook subscription that fires the voicegram.completed event to the supplied URL on every matching completed voicegram for the account.

Terminal receiver requirement

The URL MUST be a terminal receiver. The delivery worker uses redirect: 'manual' when POSTing events, so any 3xx response is treated as a delivery failure recorded as redirect_blocked: <status> in last_error (and counted toward consecutive_failures). This applies to real-event delivery, retry delivery, and the dashboard's test-event endpoint. Customers should configure their webhook endpoint to respond directly with 2xx/4xx/5xx.

Secret handling

The response includes the plaintext HMAC secret ONCE. After this response the plaintext is unrecoverable from the API; secret_prefix is the only identifier surfaced afterwards. Save the plaintext in a secure secret store on your end. If you lose it, delete the subscription and create a new one to rotate.

URL validation

  • MUST use HTTPS (http:// returns 400 invalid_url).
  • MUST be a valid URI under 2048 characters.
  • MUST NOT contain user/password credentials (https://user:pass@host).
  • MUST NOT target a private or internal host. This is validated at delivery time, not just at subscribe time.

Subscription cap

Active subscriptions per account are capped at 25. The 26th subscription returns 409 limit_reached. Deleted subscriptions do not count toward the cap.

channel_id resolution

If channel_id is provided, it must match a domain or campaign on the PAT's account. Returns 400 invalid_channel_id if no match exists, whether the id is unknown or belongs to a different account.

Endpoint

http
POST /api/v1/webhook-subscriptions

Authentication

Bearer Personal Access Token. See Authentication.

Request body

Content type: application/json (required).

FieldTypeRequiredDescription
urlstring (uri)yesHTTPS URL to POST events to. Must be a terminal receiver: 3xx responses are treated as delivery failures (redirect_blocked).
event_typeenum ("voicegram.completed")yesThe only event in v1. Required so callers do not have to change request shape when v2 adds additional event types.
sourceenum ("zapier", "dashboard")yesSource class. Drives zapier_subscription_id semantics.
channel_idstring | nullnoOptional. When null the subscription fires for all channels on the account. When set, the server derives channel_type from the id; mismatch returns 400 invalid_channel_id.
zapier_subscription_idstring | nullnoREQUIRED when source=zapier (used by the Zapier app to look up the subscription on unsubscribe). FORBIDDEN when source=dashboard.

Example: Zapier-source subscription

json
{
  "url": "https://hooks.zapier.com/hooks/standard/12345/abc",
  "event_type": "voicegram.completed",
  "channel_id": null,
  "source": "zapier",
  "zapier_subscription_id": "zap-12345-abc"
}

Example: Dashboard-source subscription scoped to a domain

json
{
  "url": "https://example.com/webhooks/voicegram",
  "event_type": "voicegram.completed",
  "channel_id": "11111111-1111-1111-1111-111111111111",
  "source": "dashboard"
}

Example request

bash
curl -X POST https://www.voicegram.io/api/v1/webhook-subscriptions \
  -H "Authorization: Bearer vg_pat_<your-token>" \
  -H "Content-Type: application/json" \
  -d '{
          "url": "https://hooks.zapier.com/hooks/standard/12345/abc",
          "event_type": "voicegram.completed",
          "channel_id": null,
          "source": "zapier",
          "zapier_subscription_id": "zap-12345-abc"
        }'

Responses

201Created

Subscription created. The response includes the plaintext HMAC secret exactly once.

FieldTypeRequiredDescription
idstring (uuid)yes
urlstring (uri)yes
secretstringyesPlaintext HMAC secret. Returned ONCE in this response. After this response the plaintext is unrecoverable from any API. Save it in a secure secret store on your end. To rotate, delete the subscription and create a new one.
secret_prefixstringyes
is_activebooleanyes
created_atstring (date-time)yes
json
{
  "id": "44444444-4444-4444-4444-444444444444",
  "url": "https://hooks.zapier.com/hooks/standard/12345/abc",
  "secret": "vg_whsec_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "secret_prefix": "vg_whsec_012",
  "is_active": true,
  "created_at": "2026-05-14T19:23:45.000Z"
}
400Bad Request

Body failed validation. The error field disambiguates:

  • validation_error: generic schema failure (see issues)
  • invalid_url: URL is not HTTPS, contains credentials, or targets a private/internal host (the reason field carries a stable diagnostic enum string)
  • invalid_channel_id: channel_id does not match any domain or campaign on the PAT's account
FieldTypeRequiredDescription
errorenum ("invalid_token", "plan_required", "validation_error", "invalid_url", "invalid_channel_id", "not_found", "rate_limited", "limit_reached", "conflict", "internal_error")yesStable error code.
messagestringnoHuman-readable error message.
reasonstringnoStable diagnostic enum string. Only set on invalid_url responses (e.g. url_must_be_https, url_resolves_to_private_host, url_must_not_contain_credentials).
issuesarray of objectnoSchema-validation issues. Only set on validation_error responses. Each entry has a path array and a message.

Example: Generic validation failure

json
{
  "error": "validation_error",
  "issues": [
    {
      "path": [
        "url"
      ],
      "message": "Invalid url"
    }
  ]
}

Example: URL targets a private host

json
{
  "error": "invalid_url",
  "message": "URL must be HTTPS and not target a private/internal host.",
  "reason": "url_resolves_to_private_host"
}

Example: channel_id not on this account

json
{
  "error": "invalid_channel_id",
  "message": "channel_id does not match any domain or campaign on your account."
}
401Unauthorized

The PAT is missing, malformed, expired, or revoked.

See Errors for the shared error response shape.

403Forbidden

PAT is valid but the account is on the free plan.

See Errors for the shared error response shape.

409Conflict

Conflict. Either the active-subscription cap of 25 has been reached (limit_reached), or a Zapier subscription with this zapier_subscription_id already exists (conflict).

FieldTypeRequiredDescription
errorenum ("invalid_token", "plan_required", "validation_error", "invalid_url", "invalid_channel_id", "not_found", "rate_limited", "limit_reached", "conflict", "internal_error")yesStable error code.
messagestringnoHuman-readable error message.
reasonstringnoStable diagnostic enum string. Only set on invalid_url responses (e.g. url_must_be_https, url_resolves_to_private_host, url_must_not_contain_credentials).
issuesarray of objectnoSchema-validation issues. Only set on validation_error responses. Each entry has a path array and a message.

Example: limit_reached

json
{
  "error": "limit_reached",
  "message": "Active subscription limit of 25 reached. Delete or disable an existing subscription before creating a new one."
}

Example: conflict

json
{
  "error": "conflict",
  "message": "A subscription with this zapier_subscription_id already exists."
}
429Too Many Requests

Per-account rate limit exceeded.

See Errors for the shared error response shape.

500Internal Server Error

Unexpected server error.

See Errors for the shared error response shape.

Need help? Email support@voicegram.io.