The Message Center - Manage Blacklist APIs

The Caller Blacklist API manages the list of callers who are blocked from reaching your numbers. A caller can be blocked from one DID, from several DIDs (one entry each), or from every DID with a single global entry. The same list is checked on every incoming call, so changes made through this API apply to the very next call.

Contents

  • Getting started
  • How responses work
  • Endpoints
  • Fields reference
  • Identifying an entry
  • Behaviour you need to understand
  • Errors
  • Warnings
  • Building an integration
  • Limits and known gaps

Getting started

Base URL

Every request goes to one URL, with the action and your token as query parameters:

https://script.google.com/macros/s/<deployment-id>/exec?action=<action>&token=<token>

Your administrator will give you the deployment ID and token.

Authentication

The token goes in the query string as token, never in a header. Authorization: Bearer headers are not read, so a request that sends the token only in a header is refused with 401 unauthorized.

The token for this API is separate from the token used by the call-time lookup. Neither works in place of the other.

Follow redirects

Every request is answered with a redirect to the actual response. Your client must follow it, and must follow it as a GET, even when the original request was a POST.

ClientSetting
PostmanSettings → Automatically follow redirects on, Follow original HTTP method off
curlUse -L. Send a body with -d and never -X POST, which makes curl repeat the POST after the redirect and fail
Most HTTP librariesFollow redirects is usually on by default; make sure the redirect is not re-sent as POST

A client that does not follow the redirect receives an HTML page instead of JSON.

Content type

Request bodies are JSON objects. Send them with Content-Type: application/json or text/plain.

First call

curl -L "https://script.google.com/macros/s/<deployment-id>/exec?action=list&limit=5&token=<token>"

A POST with a body:

curl -L "https://script.google.com/macros/s/<deployment-id>/exec?action=check&token=<token>" \
  -H "Content-Type: application/json" \
  -d '{"caller_number": "0390722790", "blocked_did": "0390722794"}'

Encode + as %2B in query strings. An unencoded + is read as a space. caller_number=%2B61390722790 is correct; caller_number=+61390722790 is not. In JSON bodies, + needs no encoding.


How responses work

The envelope

The HTTP status is always 200. The real outcome is in the body:

{ "ok": true, "status": 200, "action": "get", "...": "action-specific fields" }
{
  "ok": false,
  "status": 409,
  "action": "create",
  "error": { "code": "duplicate_entry", "message": "…", "details": { "existing": [] } }
}

Always check ok and status in the body. Do not rely on the HTTP status. ok: false always means nothing was changed.

error.code comes from a fixed list (see Errors) and is safe to branch on. error.message is for people and may change. error.details carries structured data such as the fields that are missing or the entry that conflicts.

Warnings

A successful write can carry warnings[]. A warning means the change was saved, but something about the result deserves attention, such as an entry that has no effect because a broader one already covers it. See Warnings.

Recommended fields

reason is optional but recommended. get, create and update return a recommendedFields object:

"recommendedFields": {
  "fields": ["reason"],
  "note": "reason is optional. When it is blank, a blocked call is recorded with an empty blacklistReason, so the call record will not show why the call was blocked.",
  "blank": ["reason"],
  "reminder": "Set a reason with the update action so blocked calls show why."
}

blank lists the recommended fields that are empty on this entry; reminder is empty when none are.

Entry position

Every entry includes _sheetRow, its current position in the list. It is informational only: it changes when entries above it are removed, so never use it to identify an entry. Fields starting with _ are ignored when sent, so an entry you fetched can be edited and sent back.


Endpoints

ActionMethodPurpose
listGET or POSTEvery entry, filtered and paged
getGET or POSTEvery entry for one caller
checkGET or POSTWhether a caller is blocked from a DID, and by which entry
createPOSTAdd one entry
updatePOSTChange an entry's reason
deletePOSTRemove one entry
unblockPOSTRemove every entry for one caller

list

GET ?action=list&token=<token>
GET ?action=list&global=true&limit=50&offset=0&token=<token>
ParameterDefaultMeaning
offset0Entries to skip
limitallEntries to return, 0 to 10000
caller_numberOnly this caller's entries
blocked_didOnly per-DID entries for this DID
globaltrue for global entries only, false for per-DID entries only
{
  "ok": true,
  "status": 200,
  "action": "list",
  "total": 3,
  "offset": 0,
  "returned": 1,
  "hasMore": true,
  "headers": ["caller_number", "global", "blocked_did", "reason"],
  "entries": [
    {
      "caller_number": "08017082571",
      "global": false,
      "blocked_did": "0687416874",
      "reason": "Example entry",
      "_sheetRow": 2
    }
  ],
  "recommendedFields": { "fields": ["reason"], "note": "…" }
}

total counts every entry that matches the filters; returned counts this page.

StatusMeaning
200Entries returned (possibly none)
400 invalid_filterA filter number is not a valid phone number, or global is not true or false
400 invalid_pagingoffset or limit is not a whole number in range

get

GET ?action=get&caller_number=13854463643&token=<token>

Returns every entry for the caller, global and per-DID, as an array.

{
  "ok": true,
  "status": 200,
  "action": "get",
  "key": { "type": "caller_number", "value": "13854463643" },
  "count": 1,
  "entries": [
    {
      "entry": {
        "caller_number": "13854463643",
        "global": false,
        "blocked_did": "0390722794",
        "reason": "Testing",
        "_sheetRow": 4
      },
      "recommendedFields": { "fields": ["reason"], "note": "…", "blank": [], "reminder": "" }
    }
  ]
}
StatusMeaning
200One or more entries returned
400 invalid_keycaller_number missing or not a valid phone number
404 not_foundThe caller has no entries

check

GET ?action=check&caller_number=%2B13854463643&blocked_did=0390722794&token=<token>

Answers exactly as an incoming call would be answered, and names the entry responsible. blocked_did is optional; without it, only global entries are checked, just as for a call that arrives without a DID. check always reads the current list, so it reflects changes immediately.

{
  "ok": true,
  "status": 200,
  "action": "check",
  "message": "Blocked from this DID.",
  "blocked": true,
  "scope": "did",
  "caller_number": "13854463643",
  "blocked_did": "390722794",
  "entry": { "caller_number": "13854463643", "global": false, "blocked_did": "0390722794", "reason": "Testing", "_sheetRow": 4 },
  "dataDipResponse": {
    "parameters": {
      "Blacklisted": { "value": "true", "displayToAgent": false, "saveToDatabase": true },
      "blacklistScope": { "value": "did", "displayToAgent": false, "saveToDatabase": true },
      "blacklistReason": { "value": "Testing", "displayToAgent": false, "saveToDatabase": true },
      "blacklistCaller": { "value": "13854463643", "displayToAgent": false, "saveToDatabase": true },
      "blacklistDid": { "value": "390722794", "displayToAgent": false, "saveToDatabase": true }
    }
  }
}

scope is global, did or none. entry is null when the caller is not blocked. dataDipResponse is exactly what the call-time lookup returns for this caller and DID.

StatusMeaning
200Answered, whether blocked or not
400 invalid_keycaller_number missing or invalid, or blocked_did given but invalid

create

POST ?action=create&token=<token>
{
  "caller_number": "+61 3 9999 0001",
  "blocked_did": "03 9072 2794",
  "reason": "Repeated abusive calls"
}

For a global entry, send "global": true and no blocked_did.

{
  "ok": true,
  "status": 200,
  "action": "create",
  "message": "Created the entry for 399990001 on 390722794.",
  "entry": {
    "caller_number": "399990001",
    "global": false,
    "blocked_did": "390722794",
    "reason": "Repeated abusive calls",
    "_sheetRow": 5
  },
  "recommendedFields": { "fields": ["reason"], "note": "…", "blank": [], "reminder": "" },
  "warnings": []
}

Numbers are stored in normalised form (see Identifying an entry). entry is read back after saving, so it shows exactly what was stored.

StatusMeaning
200Created (check warnings)
400 missing_required_fieldscaller_number, or blocked_did on a per-DID entry, is missing
400 unknown_fieldsA field is not one of the fields listed in details.validFields
400 validation_failedOne or more rules broken; every problem is listed in details.problems
409 duplicate_entryThe same caller already has this entry; details.existing shows it
503 busyAnother change was in progress; retry

update

POST ?action=update&caller_number=0399990001&global=true&token=<token>
POST ?action=update&caller_number=0399990001&blocked_did=0390722794&token=<token>
{ "reason": "Spam calls to every line" }

Only reason can change. Send "" to clear it. The entry's key goes in the query string; the body holds the new value. A fetched entry can be sent back as the body, as long as caller_number, global and blocked_did are unchanged.

{
  "ok": true,
  "status": 200,
  "action": "update",
  "message": "Updated the entry for 399990001 (global).",
  "changes": [{ "field": "reason", "from": "", "to": "Spam calls to every line" }],
  "entry": { "caller_number": "399990001", "global": true, "blocked_did": "", "reason": "Spam calls to every line", "_sheetRow": 6 },
  "recommendedFields": { "fields": ["reason"], "note": "…", "blank": [], "reminder": "" },
  "warnings": []
}

changes lists only real changes. Sending the reason the entry already has returns ok: true with an empty changes and writes nothing.

StatusMeaning
200Updated, or nothing to change
400 invalid_keyThe query string does not identify one entry (see Identifying an entry)
400 missing_required_fieldsreason not in the body
400 key_immutableThe body changes caller_number, global or blocked_did
400 field_not_updatableThe body changes another field that cannot be updated
400 unknown_fields / validation_failedAs for create
404 not_foundNo such entry
409 ambiguous_keyMore than one entry has this key; details.candidates lists them
503 busyRetry

delete

POST ?action=delete&caller_number=0399990001&blocked_did=0390722794&token=<token>
{ "caller_number": "0399990001", "blocked_did": "0390722794" }

The body must repeat the key as confirmation: caller_number plus the same blocked_did, or "global": true for a global entry. Any format of the same numbers is accepted.

{
  "ok": true,
  "status": 200,
  "action": "delete",
  "message": "Deleted the entry for 399990001 on 390722794.",
  "deleted": { "caller_number": "399990001", "global": false, "blocked_did": "390722794", "reason": "Repeated abusive calls", "_sheetRow": 5 },
  "warnings": [
    {
      "code": "still_blocked",
      "message": "This caller is still blocked from 390722794 by a global entry.",
      "entries": [{ "caller_number": "399990001", "global": true, "blocked_did": "", "reason": "Spam calls to every line", "_sheetRow": 5 }]
    }
  ]
}

deleted is the full entry and can be sent unchanged to create to restore it.

StatusMeaning
200Deleted (check warnings)
400 invalid_keyThe query string does not identify one entry
400 delete_confirmation_mismatchThe body does not repeat the key; details.expected shows what to send
404 not_foundNo such entry
409 ambiguous_keyMore than one entry has this key
503 busyRetry

unblock

POST ?action=unblock&caller_number=0399990001&token=<token>
{ "caller_number": "0399990001" }

Removes every entry for the caller, global and per-DID. The body must repeat caller_number.

{
  "ok": true,
  "status": 200,
  "action": "unblock",
  "message": "Removed 1 entry for caller 399990001. This caller is no longer blocked.",
  "count": 1,
  "deleted": [{ "caller_number": "399990001", "global": true, "blocked_did": "", "reason": "Spam calls to every line", "_sheetRow": 5 }],
  "warnings": []
}
StatusMeaning
200Every entry for the caller removed
400 invalid_keycaller_number missing or invalid
400 delete_confirmation_mismatchThe body does not repeat caller_number; details.entries shows what would be removed
404 not_foundThe caller has no entries
503 busyRetry

Fields reference

Required

FieldTypeRules
caller_numbertextA phone number in any common format, at least 6 digits once normalised
blocked_didtextRequired unless global is true; must be empty when it is. Same rules as caller_number

Recommended

FieldTypeRules
reasontextWhy the caller is blocked. Returned with every blocked call

Blank means no explanation. A blocked call is still blocked when reason is blank, but the call record shows an empty reason, so nobody can tell later why it was blocked.

Other fields

FieldTypeRules
globaltrue / falseDefault false. true blocks the caller from every DID. Also accepts "true", "false", 1, 0, "yes", "no"
added_bytextWho added the entry. Only available if your list has this field; otherwise refused as unknown
added_atdate2026-11-10 09:12, 2026-11-10 or ISO 8601. Only available if your list has this field

Field rules

  • Unknown fields are refused, never ignored. details.validFields lists the fields your list accepts.
  • Text cannot start with =.
  • Objects and arrays sent as values are stored as JSON text.
  • Every rule is checked before anything is saved, and every problem is reported at once in details.problems.

Identifying an entry

An entry is identified by its caller and what it blocks:

EntryKey
Per-DIDcaller_number + blocked_did
Globalcaller_number + global=true

update and delete take the key in the query string only, with exactly one of blocked_did or global=true. Both, or neither, is refused with invalid_key. get and unblock take caller_number alone and act on every entry for that caller.

Number formats

Every number you send is normalised before it is compared or stored: SIP URIs are reduced to the number inside them, punctuation is removed, and country codes and leading zeros are dropped. All of these are the same caller:

0390722790   +61 3 9072 2790   61390722790   <sip:0390722790@host;user=phone>

New entries are stored in the normalised form (390722790). Matching is always exact on the normalised digits; part of a number never matches.


Behaviour you need to understand

A global entry wins. When a caller has a global entry, they are blocked from every DID, whatever per-DID entries they also have.

  • Changes apply to the next call. Every write takes effect immediately for incoming calls.
  • Calls that arrive without a DID are checked against global entries only.
  • Withheld callers are never blocked.
  • To lift a block, delete it. There is no on/off switch. delete removes one entry; unblock removes all of a caller's entries.
  • Only the reason can be edited. To move a block to another DID, or switch between global and per-DID, delete the entry and create a new one.
  • Entries added by hand may be stored in other formats (for example 08017082571 rather than 8017082571). They match exactly the same way.

Errors

Request problems

CodeStatusMeaning
unknown_action400Action missing or not recognised; details.validActions lists them
method_not_allowed405A write was sent as GET
invalid_json400The body is not a JSON object
invalid_key400The entry or caller could not be identified
invalid_filter400A list filter is invalid
invalid_paging400offset or limit is invalid

Access

CodeStatusMeaning
unauthorized401Token missing or wrong
not_configured500The service has no token configured. Contact your administrator

Data problems

CodeStatusMeaning
missing_required_fields400details.missing lists them
unknown_fields400details.validFields lists the accepted fields
validation_failed400details.problems lists each problem with field, code and message
key_immutable400An update tried to change the entry's key
field_not_updatable400An update tried to change a field other than reason
delete_confirmation_mismatch400The body did not repeat the key

details.problems codes: invalid_number, invalid_boolean, global_with_did, invalid_date, formula_not_allowed.

Conflicts

CodeStatusMeaning
not_found404No entry for the key
duplicate_entry409The entry already exists
ambiguous_key409Several entries share this key; details.candidates lists them. Remove the extras first

Service

CodeStatusMeaning
busy503Another change was in progress for up to 10 seconds. Retry
internal_error500Unexpected error. Nothing was changed
sheet_not_found / missing_columns500The list is not set up as expected. Contact your administrator

Warnings

CodeWhen
redundant_entryOn create: a per-DID entry was added for a caller who is already blocked globally (it has no effect while the global entry exists), or a global entry was added for a caller with per-DID entries (they are now covered). entries lists the entries involved
still_blockedOn delete: the caller is still blocked, either from that DID by a global entry, or (after deleting a global entry) from the DIDs in their per-DID entries. entries lists what still blocks them

Warnings that describe call handling are worked out with the same matching rules incoming calls use.


Building an integration

function blockCaller(caller, did, reason):
    body = { caller_number: caller, reason: reason }
    if did is empty: body.global = true
    else:            body.blocked_did = did

    res = POST(base + "?action=create&token=" + token, body)   // follow redirects as GET

    if res.ok:
        for w in res.warnings: log(w.code, w.message)
        return res.entry
    if res.error.code == "duplicate_entry": return res.error.details.existing[0]
    if res.error.code == "busy": wait and retry
    raise res.error.code, res.error.message

function unblockCaller(caller):
    res = POST(base + "?action=unblock&caller_number=" + urlencode(caller) + "&token=" + token,
               { caller_number: caller })
    if res.ok or res.error.code == "not_found": return
    raise res.error.code, res.error.message

function isBlocked(caller, did):
    res = GET(base + "?action=check&caller_number=" + urlencode(caller)
              + "&blocked_did=" + urlencode(did) + "&token=" + token)
    return res.ok and res.blocked

Limits and known gaps

  • limit is at most 10000 per request.
  • Writes wait up to 10 seconds for another change to finish, then return busy.
  • Entries added by hand are not checked for duplicates or rule breaks until the API touches them. Two hand-made entries with the same key make that key ambiguous_key for update and delete.
  • The call-time lookup keeps its answers for up to 5 minutes. Changes made through this API clear that, but entries removed by hand may keep blocking for up to 5 minutes.
  • _sheetRow changes as entries are removed. Never use it as an identifier.
  • Numbers are normalised for Australia (+61) and Japan (+81). Numbers from other countries match only when written the same way every time, preferably with their country code.