Voicegram

Log a message on a Voicegram

Logs one entry (a call, text, WhatsApp message, email or note) in a Voicegram's conversation, as what your workspace did or what the sender said.

On this page

Logs one entry (a call, text, WhatsApp message, email or note) in a Voicegram's conversation, as what your workspace did or what the sender said. Voicegram never sends anything to the sender from this endpoint.

The entry is recorded as written with this token: it is always verified with the reason entered_with_api_token, its created_via is api and logged_by names the token. The label cannot be set in the request.

Send a client_message_id of your own. Sending the same one again for the same Voicegram logs nothing new and returns the existing entry with 200 and replayed: true. A new entry returns 201 and is delivered to any voicegram.message webhook subscription of the workspace. If a workflow that logs entries here also listens for them, ignore events whose created_via is api.

Part of the conversations feature: a workspace it is not available for gets 404 conversation_not_available.

Endpoint

http
POST /api/v1/voicegrams/{id}/messages

Authentication

Bearer Personal Access Token. See Authentication.

Path parameters

NameTypeRequiredDescription
idstring (uuid)yesThe Voicegram's identifier (a UUID, as returned by findVoicegramsBySender and shown in data.id of an event).

Request body

Content type: application/json (required).

FieldTypeRequiredDescription
mediumenum ("email", "sms", "whatsapp", "call", "note")yes
directionstring | nullyesoutbound for what your team said or did, inbound for what the sender said. A note has no direction and must send null; every other medium needs one.
bodystringyesThe text of the entry, 1 to 2,000 characters after control characters are stripped and surrounding whitespace is trimmed.
occurred_atstring (date-time)yesWhen it happened, ISO 8601 with a UTC offset. It must be after the Voicegram was recorded and not in the future.
client_message_idstringyesYour own identifier for this entry (a CRM note id, say). Sending the same value again for the same Voicegram logs nothing new and returns the existing entry with 200 and replayed: true.
sourceenum ("logged", "forwarded")nologged for something that happened. forwarded for an email you are passing along from the sender.
json
{
  "medium": "call",
  "direction": "outbound",
  "source": "logged",
  "body": "Called back and left a message.",
  "occurred_at": "2026-05-13T19:40:00.000Z",
  "client_message_id": "crm-note-8841"
}

Example request

curl -X POST https://www.voicegram.io/api/v1/voicegrams/:id/messages \
  -H "Authorization: Bearer vg_pat_<your-token>" \
  -H "Content-Type: application/json" \
  -d '{
          "medium": "call",
          "direction": "outbound",
          "source": "logged",
          "body": "Called back and left a message.",
          "occurred_at": "2026-05-13T19:40:00.000Z",
          "client_message_id": "crm-note-8841"
        }'

Responses

200OK

An entry with this client_message_id was already logged on this Voicegram. The existing entry is returned and nothing new is saved.

FieldTypeRequiredDescription
messageobjectyesOne entry of a Voicegram's conversation, as returned by GET and POST /api/v1/voicegrams/{id}/messages. The same fields as data.message of the voicegram.message event, plus removed_at. A removed entry stays in the list: it loses its text and its subject and keeps everything else.
message.idstringyesThe entry's own identifier.
message.mediumenum ("email", "sms", "whatsapp", "call", "note")yesWhat kind of entry it is.
message.directionstring | nullyesoutbound is your side speaking to the sender. inbound is the sender, or someone copied in on an email, speaking to you. Null for a note.
message.sourceenum ("logged", "forwarded")yeslogged: recorded as it happened. forwarded: an email a member forwarded to the Voicegram's saving address. For a forwarded entry, sender_verification describes the member who forwarded it (logged_by), not whoever wrote the message inside, which Voicegram cannot check: an automation that reads author.type recorder as "the sender replied" should also require source logged.
message.created_viaenum ("dashboard", "inbound_email", "api")yesHow the entry reached Voicegram: typed in the dashboard, received at the Voicegram's saving address, or written through this API.
message.authorobjectyesWho said it. recorder is the person who left the Voicegram. workspace is your side: a signed-in member (name is their display name) or an API token (name is null; see logged_by). participant is someone who was copied in on an email and then wrote in under their own address, which also covers an email from an address Voicegram had not seen before that a member reviewed and kept. The recorder's own address is never here: it is in data.sender.
message.author.typeenum ("workspace", "recorder", "participant")yes
message.author.namestring | nullyesA member's display name on a workspace entry. Null on every other entry.
message.author.addressstring | nullyesThe address a participant wrote from. Null on every other entry.
message.logged_byobject | nullyesWho saved the entry, when that is someone other than its author: a member who logged or forwarded it, or the API token that wrote it. Null when there is no one else.
message.sender_verificationenum ("verified", "not_verified")yesWhether Voicegram could confirm who sent this entry. Present on every entry, not only emails. not_verified means the email provider could not confirm the email came from the address it claims; the dashboard shows these as "Sender not confirmed". The entry is still saved and delivered in full. The webhooks guide (https://www.voicegram.io/docs/integrations/webhooks) says how to filter on it before an automation acts on a reply. See "Why couldn't Voicegram confirm who sent an email?" (https://www.voicegram.io/docs/workspace/sender-not-confirmed).
message.sender_verification_reasonenum ("sender_domain_authenticated", "sender_domain_not_authenticated", "entered_by_signed_in_member", "entered_with_api_token")yesWhy the entry has its label. sender_domain_authenticated: an email the provider confirmed. sender_domain_not_authenticated: an email it could not confirm (the only not_verified reason). entered_by_signed_in_member: logged by hand in the dashboard. entered_with_api_token: written through this API.
message.sender_verification_notestringyesThe same label as one plain-English sentence, derived from sender_verification_reason: "Confirmed by the email provider.", "Entered by a signed-in member.", "Entered with an API token.", or, for not_verified, "The email provider could not confirm this message came from the address it claims. The address may still be genuine, since many small domains have not set up email authentication, but anyone who has this conversation's saving address could also have sent it. Treat it as unconfirmed before automating on it."
message.recipient_toarray | nullyesThe To addresses of an emailed entry, never including the saving address. Null unless the entry is an email Voicegram received at its saving address (created_via inbound_email), so null on an email logged by hand too, and on an email saved before Voicegram kept them.
message.recipient_ccarray | nullyesThe Cc addresses of an emailed entry, never including the saving address. Null unless the entry is an email Voicegram received at its saving address (created_via inbound_email), so null on an email logged by hand too, and on an email saved before Voicegram kept them.
message.subjectstring | nullyesThe subject line of an emailed entry. Null unless the entry is an email Voicegram received at its saving address (created_via inbound_email), on an email saved before Voicegram kept it, and on a removed entry.
message.bodystring | nullyesThe text of the entry. Null on a removed entry.
message.occurred_atstring (date-time)yesWhen the entry happened. For a logged call or note it is the time given; for an email it is the time of the email.
message.created_atstring (date-time)yesWhen Voicegram saved the entry.
message.attachmentsarray of objectyesThe attachments of an emailed entry, named but never delivered. Empty for anything else.
message.attachments[].filenamestringyes
message.attachments[].content_typestring | nullyes
message.attachments[].size_bytesinteger | nullyes
message.removed_atstring | nullyesSet when a member removed the entry. A removed entry keeps its place, its author, its recipients and its time, and loses its text: body and subject are null.
replayedenum (true)yes
201Created

The entry was logged.

FieldTypeRequiredDescription
messageobjectyesOne entry of a Voicegram's conversation, as returned by GET and POST /api/v1/voicegrams/{id}/messages. The same fields as data.message of the voicegram.message event, plus removed_at. A removed entry stays in the list: it loses its text and its subject and keeps everything else.
message.idstringyesThe entry's own identifier.
message.mediumenum ("email", "sms", "whatsapp", "call", "note")yesWhat kind of entry it is.
message.directionstring | nullyesoutbound is your side speaking to the sender. inbound is the sender, or someone copied in on an email, speaking to you. Null for a note.
message.sourceenum ("logged", "forwarded")yeslogged: recorded as it happened. forwarded: an email a member forwarded to the Voicegram's saving address. For a forwarded entry, sender_verification describes the member who forwarded it (logged_by), not whoever wrote the message inside, which Voicegram cannot check: an automation that reads author.type recorder as "the sender replied" should also require source logged.
message.created_viaenum ("dashboard", "inbound_email", "api")yesHow the entry reached Voicegram: typed in the dashboard, received at the Voicegram's saving address, or written through this API.
message.authorobjectyesWho said it. recorder is the person who left the Voicegram. workspace is your side: a signed-in member (name is their display name) or an API token (name is null; see logged_by). participant is someone who was copied in on an email and then wrote in under their own address, which also covers an email from an address Voicegram had not seen before that a member reviewed and kept. The recorder's own address is never here: it is in data.sender.
message.author.typeenum ("workspace", "recorder", "participant")yes
message.author.namestring | nullyesA member's display name on a workspace entry. Null on every other entry.
message.author.addressstring | nullyesThe address a participant wrote from. Null on every other entry.
message.logged_byobject | nullyesWho saved the entry, when that is someone other than its author: a member who logged or forwarded it, or the API token that wrote it. Null when there is no one else.
message.sender_verificationenum ("verified", "not_verified")yesWhether Voicegram could confirm who sent this entry. Present on every entry, not only emails. not_verified means the email provider could not confirm the email came from the address it claims; the dashboard shows these as "Sender not confirmed". The entry is still saved and delivered in full. The webhooks guide (https://www.voicegram.io/docs/integrations/webhooks) says how to filter on it before an automation acts on a reply. See "Why couldn't Voicegram confirm who sent an email?" (https://www.voicegram.io/docs/workspace/sender-not-confirmed).
message.sender_verification_reasonenum ("sender_domain_authenticated", "sender_domain_not_authenticated", "entered_by_signed_in_member", "entered_with_api_token")yesWhy the entry has its label. sender_domain_authenticated: an email the provider confirmed. sender_domain_not_authenticated: an email it could not confirm (the only not_verified reason). entered_by_signed_in_member: logged by hand in the dashboard. entered_with_api_token: written through this API.
message.sender_verification_notestringyesThe same label as one plain-English sentence, derived from sender_verification_reason: "Confirmed by the email provider.", "Entered by a signed-in member.", "Entered with an API token.", or, for not_verified, "The email provider could not confirm this message came from the address it claims. The address may still be genuine, since many small domains have not set up email authentication, but anyone who has this conversation's saving address could also have sent it. Treat it as unconfirmed before automating on it."
message.recipient_toarray | nullyesThe To addresses of an emailed entry, never including the saving address. Null unless the entry is an email Voicegram received at its saving address (created_via inbound_email), so null on an email logged by hand too, and on an email saved before Voicegram kept them.
message.recipient_ccarray | nullyesThe Cc addresses of an emailed entry, never including the saving address. Null unless the entry is an email Voicegram received at its saving address (created_via inbound_email), so null on an email logged by hand too, and on an email saved before Voicegram kept them.
message.subjectstring | nullyesThe subject line of an emailed entry. Null unless the entry is an email Voicegram received at its saving address (created_via inbound_email), on an email saved before Voicegram kept it, and on a removed entry.
message.bodystring | nullyesThe text of the entry. Null on a removed entry.
message.occurred_atstring (date-time)yesWhen the entry happened. For a logged call or note it is the time given; for an email it is the time of the email.
message.created_atstring (date-time)yesWhen Voicegram saved the entry.
message.attachmentsarray of objectyesThe attachments of an emailed entry, named but never delivered. Empty for anything else.
message.attachments[].filenamestringyes
message.attachments[].content_typestring | nullyes
message.attachments[].size_bytesinteger | nullyes
message.removed_atstring | nullyesSet when a member removed the entry. A removed entry keeps its place, its author, its recipients and its time, and loses its text: body and subject are null.
replayedenum (false)yes
json
{
  "message": {
    "id": "55555555-5555-5555-5555-555555555555",
    "medium": "call",
    "direction": "outbound",
    "source": "logged",
    "created_via": "api",
    "author": {
      "type": "workspace",
      "name": null,
      "address": null
    },
    "logged_by": {
      "type": "api",
      "name": "CRM sync"
    },
    "sender_verification": "verified",
    "sender_verification_reason": "entered_with_api_token",
    "sender_verification_note": "Entered with an API token.",
    "recipient_to": null,
    "recipient_cc": null,
    "subject": null,
    "body": "Called back and left a message.",
    "occurred_at": "2026-05-13T19:40:00.000Z",
    "created_at": "2026-05-13T19:40:03.000Z",
    "removed_at": null,
    "attachments": []
  },
  "replayed": false
}
400Bad Request

The body failed validation (validation_error): a field is missing or the wrong type, an unlisted field was sent (including sender_verification, sender_verification_reason and sender_verification_note), a note has a direction or another medium has none, the text is empty or over 2,000 characters, or occurred_at is before the Voicegram was recorded or in the future.

FieldTypeRequiredDescription
errorenum ("invalid_token", "plan_required", "validation_error", "invalid_url", "invalid_channel_id", "not_found", "rate_limited", "limit_reached", "conflict", "forbidden", "conversation_not_available", "conversation_daily_cap", "conversation_participant_cap", "conversation_not_a_stranger", "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.
json
{
  "error": "validation_error",
  "issues": [
    {
      "path": [],
      "message": "Unrecognized key(s) in object: 'sender_verification'"
    }
  ]
}
401Unauthorized

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

See Errors for the shared error response shape.

403Forbidden

The account is on the free plan (plan_required), or the workspace is closed (forbidden).

See Errors for the shared error response shape.

404Not Found

No Voicegram with this id on the PAT's account (not_found), or conversations are not available for the workspace (conversation_not_available).

See Errors for the shared error response shape.

409Conflict

The Voicegram cannot have a conversation yet: it is still processing, or it has expired (conversation_not_available).

FieldTypeRequiredDescription
errorenum ("invalid_token", "plan_required", "validation_error", "invalid_url", "invalid_channel_id", "not_found", "rate_limited", "limit_reached", "conflict", "forbidden", "conversation_not_available", "conversation_daily_cap", "conversation_participant_cap", "conversation_not_a_stranger", "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.
json
{
  "error": "conversation_not_available",
  "message": "This Voicegram can't have a conversation: it is still processing, or it has expired."
}
429Too Many Requests

Per-account rate limit exceeded (rate_limited), or the conversation or the workspace reached its daily limit for logged entries (conversation_daily_cap).

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.