oqimDocs
API referenceConsole

Start here

  • Introduction
  • Getting started

Guides

  • Telegram accounts
  • Recipients
  • Campaigns
  • Scheduling
  • Webhooks
  • AI writing assistant

AI Seller

  • AI Seller
  • Channels
  • Live inbox
  • Past conversations and privacy
  • Business insights

API

  • Authentication
  • API reference
  • SDK
  • Errors
  • Rate limits

Trust and operations

  • Security
  • Compliance
  • Administration
Docs / Recipients

Start here

  • Introduction
  • Getting started

Guides

  • Telegram accounts
  • Recipients
  • Campaigns
  • Scheduling
  • Webhooks
  • AI writing assistant

AI Seller

  • AI Seller
  • Channels
  • Live inbox
  • Past conversations and privacy
  • Business insights

API

  • Authentication
  • API reference
  • SDK
  • Errors
  • Rate limits

Trust and operations

  • Security
  • Compliance
  • Administration

Recipients

A recipient is a person your organization may message on Telegram, with the basis you have for doing so. Oqim sends only to recipients with a recorded basis, and never to anyone who opted out.

Fields#

FieldDescription
telegram_idNumeric Telegram user ID. Each recipient needs a Telegram ID, a username, or both.
usernamePublic Telegram username, without the @.
first_name, last_nameUsed in message variables. Either may be empty.
languageOptional language code, such as uz, ru or en.
consent_statusThe basis for contact; see below. Defaults to UNKNOWN.
consent_timestampWhen the basis was recorded.
consent_noteWhere it came from, e.g. Registered for Tashkent Expo 2026 on expo.example.
attributesAny other data as key–value pairs. Each key is a message variable, e.g. {{event_date}}.
tagsFree-form labels for building audiences: up to 50, of up to 64 characters each.
sourceHow the recipient arrived: CSV, JSON, API, MANUAL or CRM.
opted_out_at, opt_out_sourceSet when the person opts out.

A Telegram ID and a username are each unique within an organization. Importing someone who already exists updates them instead of creating a duplicate.

Consent states#

StatusMeaningReceives campaigns
OPTED_INThey explicitly asked to receive messages from you.Yes
AUTHORIZEDThey authorized contact, for example by ticking a box on a sign-up form.Yes
EXISTING_RELATIONSHIPCustomers, members or attendees you already serve.Yes
UNKNOWNNo basis recorded.No, unless the platform allows unknown consent (off by default)
OPTED_OUTThey opted out. Permanent.Never

Record the basis truthfully: it's what you would show if a recipient, Telegram or a regulator asked why someone received a message. See Compliance.

Reaching people by ID or username#

Telegram only lets an account message a user by numeric ID when the account already knows them: they're in its contacts or have written to it. Otherwise the send ends as unreachable with PEER_UNRESOLVABLE. A public username always works, so include usernames when you have them. If a username now belongs to a different Telegram user than the ID on file, Oqim doesn't send (USERNAME_REASSIGNED).

Importing#

Recipients → Import takes a CSV or JSON file of up to 10 MB and 100,000 rows. The same endpoint runs a dry run and the real import, so you see exactly what will happen before anything is saved.

CSV format

  • A header row, UTF-8. Commas, semicolons and tabs are all recognised as the separator.
  • telegram_id or username on every row; both is fine. Usernames may include the @ or be a t.me/ link. They must be 4 to 32 letters, digits or underscores and start with a letter.
  • Optional: first_name, last_name, language, consent_status, consent_timestamp (a date, or a date and time) and tags separated by semicolons. Headers are case-insensitive, and common variants work: tg_id, name, surname, consent, lang.
  • Any other column becomes an attribute you can use as a variable. Its name is lowercased, with other characters turned into underscores: a column Event Date becomes {{event_date}}.
  • Rows without a consent status get the import's default consent, which is UNKNOWN unless you choose another. Consent values are case-insensitive.
expo-attendees.csv
telegram_id,username,first_name,last_name,consent_status,consent_timestamp,tags,event_date,location
123456789,aziza_k,Aziza,Karimova,OPTED_IN,2026-08-14,expo-2026;vip,1 October,Tashkent City Hall
987654321,,Timur,Rakhimov,EXISTING_RELATIONSHIP,,expo-2026,1 October,Tashkent City Hall
,nodira_s,Nodira,Saidova,,,expo-2026,1 October,Samarkand Pavilion

Here event_date and location become attributes, and Nodira, with no consent status, gets the default. If a spreadsheet shows long IDs as 1.23E+08, format the column as text before exporting; Oqim rejects IDs in scientific notation rather than guessing.

Dry run, then commit

Post the file as multipart/form-data with dry_run=true. Oqim checks every row and returns a report without writing anything:

Shell
curl https://app.example.com/api/v1/recipients/import \
  -H "Authorization: Bearer $OQIM_API_KEY" \
  -F file=@expo-attendees.csv \
  -F dry_run=true \
  -F default_consent=UNKNOWN \
  -F new_list_name="Expo 2026 attendees"
Response · 200 OK
{
  "dry_run": true,
  "total": 1950,
  "valid": 1923,
  "created": 0,
  "updated": 0,
  "duplicates": 12,
  "invalid": 9,
  "suppressed": 6,
  "opted_out": 6,
  "by_consent": { "OPTED_IN": 1402, "EXISTING_RELATIONSHIP": 490, "UNKNOWN": 25, "OPTED_OUT": 6 },
  "issues": [
    { "row": 14, "field": "telegram_id", "value": "12ab", "kind": "INVALID", "message": "Telegram IDs are positive whole numbers." },
    { "row": 402, "kind": "SUPPRESSED", "message": "On the suppression list, so imported as opted out." }
  ],
  "columns": ["telegram_id", "username", "first_name", "last_name", "consent_status", "consent_timestamp", "tags", "attributes.event_date", "attributes.location"],
  "sample": []
}

Send the same file with dry_run=false to import it. The response has the same shape with created, updated and an import_id; past imports are listed at GET /recipient-imports. A report lists at most 500 issues.

Form fieldDescription
fileThe CSV or JSON file. Required.
dry_runtrue to validate only, false to import.
default_consentConsent status for rows that don't have one. Default UNKNOWN.
tagsTags added to every imported recipient, separated by commas or semicolons.
list_id or new_list_nameAdd everyone imported to an existing list, or to a new one. Not both.
sourceRecorded on each recipient: CSV or JSON by default, or API, CRM, MANUAL.

Integrations can skip the file: send application/json with the rows in records, using the same keys as the CSV columns (tags may be an array and attributes an object), plus the fields above. source then defaults to API.

POST /recipients/import (JSON)
{
  "dry_run": false,
  "list_id": "lst_01j9r5e2g4j6m8p0r2t4w6y8a0",
  "records": [
    { "username": "aziza_k", "first_name": "Aziza", "consent_status": "OPTED_IN", "tags": ["expo-2026"], "attributes": { "location": "Hall B" } }
  ]
}

Validation issues

Each issue names the row (the header counts as row 1, as in a spreadsheet), the field and value when relevant, a kind and a readable message:

KindWhat happenedImported
INVALIDThe row can't be used: no Telegram ID or username, a malformed ID or username, an unknown consent value or a consent time in the future.No
DUPLICATEThe same person appears earlier in the file. The first row wins.No
SUPPRESSEDThe person is on your suppression list.As opted out
OPTED_OUTThe row says OPTED_OUT, or the person opted out earlier; an import can't opt them back in.As opted out
UNKNOWN_CONSENTNo basis recorded. Campaigns skip them unless the platform allows unknown consent.Yes

Lists, tags and bulk changes#

Lists are named groups you add people to; tags are labels on the recipient. A campaign's audience is the union of its lists, everyone carrying any of its tags, and individually picked recipients. Opted-out and suppressed people are left out when the campaign launches and checked again right before each send, whatever the audience says. Deleting a list keeps its recipients.

POST /recipients/bulk applies one action to up to 5,000 recipients: tag, untag, add_to_list, remove_from_list, set_consent, opt_out or delete. set_consent can't opt anyone out; that's what opt_out is for.

Keep recipients in sync with your customer database#

To mirror contacts from a CRM or your own database, send them to POST /recipients/import as JSON, on a schedule or whenever they change. It's the endpoint that updates as well as creates: each record is matched to an existing recipient by Telegram ID, then by username, and updated; a record that matches no one creates a recipient. POST /recipients only creates, and answers 409 recipient_exists for anyone you already have, so keep it for one-off additions.

Shell
curl https://app.example.com/api/v1/recipients/import \
  -H "Authorization: Bearer $OQIM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "CRM",
    "new_list_name": "Active customers 2026-09-27",
    "records": [
      { "telegram_id": 123456789, "username": "aziza_k", "first_name": "Aziza", "consent_status": "OPTED_IN",
        "consent_timestamp": "2026-08-14", "attributes": { "crm_id": "C-1042", "tier": "gold" } },
      { "username": "timur_r", "first_name": "Timur", "consent_status": "OPTED_OUT", "attributes": { "crm_id": "C-2210" } }
    ]
  }'

The response is the import report (201 Created) with how many recipients were created and updated. Send "dry_run": true first to get the same report without saving anything. When a record matches an existing recipient:

FieldOn update
telegram_idFilled in if the recipient had none. An existing ID is never replaced.
usernameReplaced, unless another recipient already has that username.
first_name, last_name, language, consent_timestampReplaced when the record has a value; empty or missing values keep what's stored.
consent_statusReplaced only when the record has one: default_consent applies to new recipients only. No one leaves OPTED_OUT, and a record that says OPTED_OUT opts the person out for good.
tagsAdded to the recipient's tags. An import never removes a tag.
attributesMerged key by key. Keys you don't send keep their values.
  • An import never deletes anyone. For people who left your database, use POST /recipients/bulk with delete or remove_from_list and their Oqim IDs; GET /recipients?q= with a Telegram ID or username finds them. Simpler still, import each sync into a new list with new_list_name and aim campaigns at the latest one: whoever is missing from your export drops out of the audience. Deleting an old list keeps its recipients.
  • Opt-outs go both ways. Subscribe a webhook to recipient.opted_out, then read GET /recipients/{id} for the Telegram ID, username and attributes such as crm_id to update your side. Opt-outs your side records come in as OPTED_OUT records, as above.
  • One request takes up to 100,000 records and 10 MB, and an organization's imports run one at a time. An import that would take you past your plan's recipient limit is refused (403 plan_limit_reached) before anything is saved.

Opt-out#

Add {{opt_out_url}} to a message and every recipient gets a personal link such as https://app.example.com/u/b3JnXzAxajlyMm02azRu.q8Zr0mX2pLk5Vd7nWc3TyA. The token is signed, so it can't be guessed or altered, and it keeps working for as long as the recipient exists. The page asks the person to confirm, in Uzbek, Russian or English depending on their browser, with a switcher for the others.

An opt-out is permanent and applies to the whole organization. The moment it's confirmed:

  • the recipient's consent becomes OPTED_OUT and they're added to the suppression list;
  • sends to them that haven't gone out yet, in any campaign, are skipped;
  • recipient.opted_out is emitted, so your own systems can record it too.

When someone asks to stop in another way, by email or in a reply, record it with Opt out on the recipient, or POST /recipients/{id}/opt-out. If they aren't a recipient yet, add them to the suppression list.

Opt-outs can't be undone

Nothing in Oqim opts a person back in: not an import, not an edit, not removing their suppression entry. If they want your messages again, they need to tell you through your own channels, and you record that as a new contact.

Suppression list#

The suppression list holds Telegram IDs and usernames your organization must never message: everyone who opted out, plus anyone you add. It outlives deleted recipients, and future imports of the same person arrive opted out. Each entry records its source: OPT_OUT_LINK, MANUAL, API, IMPORT or ADMIN.

Shell
curl https://app.example.com/api/v1/suppressions \
  -H "Authorization: Bearer $OQIM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"username": "timur_r", "reason": "Asked by email on 24 Sep to stop"}'

Adding an entry opts out any existing recipient with that ID or username at once; the response says how many. You can remove entries you added by mistake, but removing one never restores consent: recipients who opted out stay opted out.

PreviousTelegram accountsNext Campaigns