SMS

Consent (CASL)

Every SMS, transactional or commercial, needs a consent record (express or implied) for the recipient, and the API refuses a send without one with 451 NO_CONSENT. CASL itself requires consent for commercial messages; HonkIO requires it for every text.

CASL Consent (required before sending)

Every SMS, transactional or commercial, must have a consent record (express or implied) for the recipient. The API refuses a send without one with HTTP 451 NO_CONSENT. CASL itself requires consent for commercial messages; HonkIO requires it for every text.

const { error } = await honkio.consents.create({
  phoneNumber: '+1613XXXXXXX',
  consentType: 'express',
  sourceDescription: 'Website opt-in form',
  sourceIp: '203.0.113.1',
})
if (error) throw new Error(error.message)

Implied consent for an existing customer, with the clock running from their last transaction:

const { data, error } = await honkio.consents.create({
  phoneNumber: '+1613XXXXXXX',
  consentType: 'implied',
  relationshipType: 'purchase',
  lastTransactionDate: '2025-11-04',
})
if (error) throw new Error(error.message)
console.log(data.expires_at) // '2027-11-04T00:00:00.000Z'

Express consent never expires. Implied consent expires two years after a purchase, lease, contract, donation, volunteer work or membership, and six months after an inquiry or application (CASL s. 10(10), 10(13)); HonkIO reads which from relationship_type, and a type that names neither gets six months. Pass last_transaction_date so the clock runs from the real relationship rather than from the day you record it, or set expires_at outright when you have already worked out the expiry. The response echoes expires_at so you can verify it.

Bulk upload

In the dashboard, open Compliance and choose Upload a consent list. Pick a CSV with a phone column, say how you got the contacts, and each number is recorded with that consent. Other columns are ignored, so a CRM export works as it is.

From code, send up to 5,000 rows at once. The defaults apply to every row, and a row's own fields override them. Every upload is kept as a consent batch you can look up later.

const { data, error } = await honkio.consents.recordBatch({
  defaults: { consentType: 'implied', relationshipType: 'volunteer', lastTransactionDate: '2026-05-01' },
  rows: [
    { phoneNumber: '+1613XXXXXXX' },
    { phoneNumber: '+1613XXXXXXX', consentType: 'express', sourceDescription: 'Petition form' },
  ],
  fileName: 'volunteers.csv',
})
if (error) throw new Error(error.message)
console.log(data.counts) // { rows: 2, recorded: 2, already_recorded: 0, opted_out: 0, ... }
  • recorded: the consent is on file.
  • already_recorded: an active consent at least as strong was already on file, so sending the same file twice records nothing twice.
  • opted_out: the number has opted out. An upload never re-consents it.
  • revoked_by_review: HonkIO revoked the number's consent after reviewing an earlier upload. No upload, and no single record (409 CONSENT_REVOKED_BY_REVIEW), brings it back unless HonkIO lifts the block.
  • duplicate_in_file: the number appeared on an earlier row.
  • invalid: the phone number did not parse, a date is not a real YYYY-MM-DD, the row is missing what its consent type needs, or its implied consent has already lapsed (its two years, or six months for an inquiry, have passed since its date). The reason names the field.

Test and live records

Consents and opt-outs are kept per key mode. A consent recorded with a test key (mk_test_) lets test sends through and never a live one; live sends are checked against the consents a live key recorded. Opt-outs and revokes work the same way: one made with a test key blocks test sends only. Each key lists, checks and uploads its own mode's records, so a test key never sees your live consents, and in the dashboard Compliance follows the Live/Test switch. A STOP texted to one of your numbers is always live. To try a send in test mode, first record a consent for the number with your test key.

Erasure needs a live key and deletes the number's consents in both modes. Opt-outs are kept in both.