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. Your endpoint must respond directly with 2xx/4xx/5xx; a 3xx response is treated as a delivery failure (recorded as redirect_blocked). This applies to real-event delivery, retry delivery, and the dashboard's test-event endpoint.

Secret handling

The response includes the plaintext signing 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).
  • The URL must not point to a private or internal host.

Subscription cap

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

channel_id resolution

If channel_id is provided, it must be a domain or campaign on your account. Returns 400 invalid_channel_id if no match exists.

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.
sourceenum ("zapier", "dashboard")yesWhether the subscription was created via Zapier or the dashboard. Send zapier or dashboard; this governs whether zapier_subscription_id is required.
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. 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

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 signing secret exactly once.

FieldTypeRequiredDescription
idstring (uuid)yes
urlstring (uri)yes
secretstringyesPlaintext signing 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_prefixstringyesFirst 12 characters of the signing secret (e.g. vg_whsec_012). Safe to display in lists, logs, and dashboards.
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 string. Only set on invalid_url responses. Treat this as an open set and match on the prefix before the colon when one is present. Bare (colon-less) values: url_parse_failed, scheme_must_be_https, url_must_not_contain_credentials, hostname_is_localhost, dns_lookup_returned_no_addresses. Address/message-suffixed values of the form <reason>: <detail>: host_resolves_to_private_ip: 10.0.0.5, ipv4_is_private_or_loopback: 127.0.0.1, ipv6_is_private_or_loopback: ::1, and dns_lookup_failed: <message>.
issuesarray of objectnoSchema-validation issues. Only set on validation_error responses. Each entry has a path array and a message.

Example: 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": "host_resolves_to_private_ip: 10.0.0.5"
}

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 string. Only set on invalid_url responses. Treat this as an open set and match on the prefix before the colon when one is present. Bare (colon-less) values: url_parse_failed, scheme_must_be_https, url_must_not_contain_credentials, hostname_is_localhost, dns_lookup_returned_no_addresses. Address/message-suffixed values of the form <reason>: <detail>: host_resolves_to_private_ip: 10.0.0.5, ipv4_is_private_or_loopback: 127.0.0.1, ipv6_is_private_or_loopback: ::1, and dns_lookup_failed: <message>.
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.