Bitta API. Barcha xabarlar.
Integratsiyani boshlang, so‘rovlarni sinang va kerakli javoblarni bir joydan toping.
Integratsiya usulini tanlang
Boshlashdan oldin: autentifikatsiya va RSA imzosi
cloudme/notify-laravel
Laravel ilovalaringiz uchun rasmiy integratsiya - RSA imzolash, OAuth token, retry va Idempotency-Key avtomatik boshqariladi.
composer require cloudme/notify-laravelO‘rnatish
Paketni Composer orqali o‘rnating:
composer require cloudme/notify-laravelLaravel'ning package auto-discovery mexanizmi cloudme/notify-laravelning composer.json faylidagi extra.laravel ma’lumotlari asosida NotifyServiceProviderni va Notify facade'ini avtomatik ro‘yxatdan o‘tkazadi. config/app.phpga qo‘lda hech narsa qo‘shish shart emas.
Konfiguratsiya
.env fayliga quyidagilarni qo‘shing:
NOTIFY_ENVIRONMENT=sandbox
NOTIFY_CLIENT_ID=your_client_id
NOTIFY_API_KEY=your_api_key
NOTIFY_PRIVATE_KEY_PATH=storage/app/notify/private.pem
NOTIFY_TOKEN_STORE=cache
NOTIFY_LOCALE=uz- Xususiy kalit serverda xavfsiz saqlanishi kerak (masalan,
storage/app/notify/-public/emas). - Uni hech qachon Git repository'ga commit qilmang.
- Notify platformasida sizning xususiy kalitingiz emas, faqat unga mos ochiq (public) kalit saqlanadi - server sizning imzongizni shu ochiq kalit bilan tekshiradi.
- Production sirlarini (client_id, api_key, xususiy kalit) hech qachon dokumentatsiya yoki
.env.exampleichiga yozmang.
Sandbox va Production
NOTIFY_ENVIRONMENT qiymati asosiy URL'ni avtomatik tanlaydi - uni qo‘lda yozish shart emas:
| Muhit | Base URL |
|---|---|
| sandbox | https://sandbox.notify.cloudme.uz/api/v1 |
| production | https://api.notify.cloudme.uz/api/v1 |
Sandbox'da yuborilgan xabarlar uchun narx hisoblanib response'da qaytarilishi mumkin, lekin real balansdan pul yechilmaydi va real SMS/xabar yuborilmaydi. Notify serveri sizning client_id/api_key'ingiz haqiqatan ham shu muhitga tegishli ekanini tekshiradi - sandbox va production credentiallarini aralashtirib bo‘lmaydi.
Diagnostika: notify:doctor
Konfiguratsiyani real xabar yubormasdan tekshirish uchun:
php artisan notify:doctorMuvaffaqiyatli natija namunasi:
Environment ........ sandbox
Base URL ........... https://sandbox.notify.cloudme.uz/api/v1
Client ID .......... configured
API key ............ configured
Private key file ... readable
Token store ........ cache
Cache store reachable yes
All checks passed. No messages were sent.SMS yuborish
Container'dan NotifyClientni oling va yuboring:
use CloudMe\Notify\NotifyClient;
$notify = app(NotifyClient::class);
$response = $notify->sms()->send(
'+998901234567',
'Salom! Bu CloudMe Notify orqali yuborilgan SMS.'
);Yoki Notify facade orqali:
use CloudMe\NotifyLaravel\Facades\Notify;
Notify::sms()->send(
'+998901234567',
'Salom! Bu CloudMe Notify orqali yuborilgan SMS.'
);$response - CloudMe\Notify\Responses\SendMessageResponse obyekti:
$response->messageId;
$response->status;
$response->price;
$response->currency;
$response->balance;
$response->environment;send()ning birinchi argumenti - string qabul qiluvchi, massiv emas:
// Noto'g'ri - send()ning birinchi argumenti massiv emas, string:
$notify->sms()->send([
'to' => '+998901234567',
'message' => 'Salom!',
]);Xabar holatini tekshirish
$status = $notify->message($response->messageId);
$status->messageId;
$status->channel;
$status->recipient;
$status->status;
$status->smsType;
$status->price;
$status->currency;
$status->errorCode;
$status->createdAt;
$status->sentAt;
$status->deliveredAt;
$status->environment;Xabar hayotiy sikli davomida status quyidagi qiymatlardan birini oladi (Notify REST API'dagi haqiqiy qiymatlar):
createdmoderationqueuedprocessingsentdeliveredfailedrejectedcancelled
Balansni tekshirish
$balance = $notify->balance();
$balance->balance;
$balance->reservedBalance;
$balance->currency;Shablonsiz xabarlar moderatsiyasi
Shablon (templateId) bilan yuborilgan xabar darhol ketadi. Oddiy matn tizim shablonlari va kompaniyangiz shablonlari bilan solishtiriladi: biror shablonga mos kelsa ({{o'zgaruvchi}} o'rnida istalgan qiymat bo'lishi mumkin), u ham darhol yuboriladi.
Hech bir shablonga mos kelmagan matn moderatsiyaga tushadi: status = moderation, mablag' yechilgan, lekin xabar hali yuborilmagan. Moderator tasdiqlasa - queued, rad etsa - rejected (mablag' qaytariladi, message.rejected webhook yuboriladi). Moderator shu matndan kompaniyangiz uchun shablon yaratishi mumkin - keyingi safar bunday xabar moderatsiyasiz ketadi.
$response = Notify::sms()->send('+998901234567', 'Yozgi chegirma -50%!');
$response->status; // "moderation"
$response->isHeldForModeration(); // true
Notify::message($response->messageId)->isRejected();OTP - bir martalik kod yuborish va tekshirish
Kodni platforma o'zi yaratadi, yuboradi va tekshiradi - ilovangiz kodni ko'rmaydi va saqlamaydi. Kod uzunligi, amal qilish muddati va urinishlar soni Notify kabinetidagi kompaniyangizning OTP sozlamalaridan, ular belgilanmagan bo'lsa - administrator belgilagan standart qiymatlardan olinadi. OTP oddiy xabar narxida hisoblanadi va moderatsiyaga tushmaydi.
use CloudMe\Notify\Exceptions\RateLimitException;
use CloudMe\NotifyLaravel\Facades\Notify;
// 1. Kod yuborish - otp_id ni saqlab qo'ying
try {
$otp = Notify::otp()->send(to: '+998901234567'); // channel: 'sms' | 'telegram' | 'whatsapp' | 'email'
} catch (RateLimitException $e) {
// OTP_RESEND_TOO_SOON - $e->retryAfterSeconds
}
session(['otp_id' => $otp->otpId]);
$otp->codeLength; // 6
$otp->expiresIn; // 300
$otp->maxAttempts; // 3
$otp->code; // production'da null, sandbox'da kodning o'zi (test uchun)
// 2. Foydalanuvchi kiritgan kodni tekshirish
$result = Notify::otp()->verify(session('otp_id'), $request->code);
if ($result->verified) {
// telefon tasdiqlandi
} elseif ($result->canRetry()) {
// kod noto'g'ri - $result->attemptsLeft ta urinish qoldi
} else {
// muddati o'tgan / urinishlar tugagan / ishlatilgan - yangi kod yuboring
}- Noto'g'ri, muddati o'tgan yoki ishlatilgan kod exception emas, natija sifatida qaytadi. Noma'lum
otp_id-NotFoundException. - Shu raqamga yangi kod yuborilsa, avvalgisi bekor bo'ladi. Juda tez qayta so'ralsa -
RateLimitException(OTP_RESEND_TOO_SOON,retryAfterSeconds). send()vaverify()avtomatik qayta yuborilmaydi: aks holda ikkinchi kod ketishi yoki urinish ikki marta sanalishi mumkin.- API client'da kanalning yuborish scope'i bo'lishi kerak (masalan
sms:send).
REST API orqali
POST /api/v1/otp/send
{ "to": "998901234567", "channel": "sms" }
200 { "otp_id": "0f6c1a52-...", "status": "pending", "code_length": 6,
"max_attempts": 3, "expires_in": 300, "message_id": "...", "price": "120.0000" }
POST /api/v1/otp/verify
{ "otp_id": "0f6c1a52-...", "code": "481201" }
200 { "verified": true, "status": "verified" }
422 { "verified": false, "attempts_left": 2, "error": { "code": "OTP_INVALID" } }Bot yoki Firebase loyihasini tanlash
Kanallar bo‘limidagi profil ID sini kiriting. FCM tokenini ro‘yxatdan o‘tkazish va xabar yuborishda bir xil loyiha ID sidan foydalaning. Har bir botda mijoz alohida bog‘lanadi. SDK 1.3 relizi uchun.
$notify->telegram()->send(to: "998901234567", message: "Hello", channelAccountId: 7);
$notify->push()->registerDevice("998901234567", $fcmToken, channelAccountId: 12);
$notify->push()->send(to: "998901234567", message: "Hello", channelAccountId: 12);
NotifyMessage::make()->channel("telegram")->channelAccount(7)->message("Hello");Boshqa kanallar va entry point'lar
Har bir kanal - haqiqiy SDK manba kodidan tekshirilgan, ishlaydigan misollar:
Telegram
$notify->telegram()->send('123456789', 'Buyurtmangiz tayyor.');Telegram telefon raqamiga mijoz botni bir marta ochmaguncha yoza olmaydi. Avval havolani yuboring (7 kun amal qiladi):
$binding = $notify->telegram()->bindingLink('+998901234567');
if (! $binding->bound) {
$notify->sms()->send('+998901234567', "Telegram: {$binding->link}");
}$notify->whatsapp()->send('+998901234567', 'Buyurtmangiz tayyor.');Voice (ovozli qo‘ng‘iroq)
Voice kanali send()ning o‘rniga o‘qilishi qulayroq call() aliasidan foydalanadi:
$notify->voice()->call('+998901234567', 'Sizning buyurtmangiz tayyor.');subject - send()ning oltinchi (named) argumenti, shuning uchun named argument sifatida berilishi kerak:
$notify->email()->send(
to: 'customer@example.com',
message: 'Buyurtmangiz tayyor.',
subject: 'Buyurtma holati'
);Push
$notify->push()->send(
to: '+998901234567',
message: 'Buyurtmangiz tayyor.',
subject: 'Buyurtma tayyor'
);Push yuborishdan oldin mijozning FCM device tokenini ro‘yxatdan o‘tkazish kerak:
$notify->push()->registerDevice(
phone: $user->phone,
fcmToken: $request->fcm_token
);Shablon (template) va o‘zgaruvchilar
$notify->sms()->send(
to: '+998901234567',
templateId: 42,
variables: ['name' => 'Aziz', 'order_id' => '10293']
);Foydalanish mumkin bo‘lgan shablonlar ro‘yxati:
foreach ($notify->templates()->list('sms') as $template) {
$template->id; // templateId
$template->variables; // ['name', 'order_id']
}Idempotency-Key
Bir xil idempotencyKey bilan takroriy so‘rov xabarni ikki marta yubormaydi - o‘rniga birinchi natija qaytariladi. Bermasangiz, SDK o‘zi avtomatik generatsiya qiladi.
$notify->sms()->send(
to: '+998901234567',
message: 'Buyurtmangiz tayyor.',
idempotencyKey: (string) $order->id
);Barcha entry point'lar
| Metod | Vazifasi |
|---|---|
sms() / telegram() / whatsapp() / voice() / email() | ChannelSender qaytaradi - ->send() (voice uchun ->call()) |
push() | PushChannelSender qaytaradi - ->send() va ->registerDevice() |
channel(string $channel) | Hali nomlangan metodi yo‘q kanal uchun umumiy "escape hatch" - generic ChannelSender qaytaradi |
message(string $messageId) | Bitta xabarning holatini qaytaradi (MessageStatusResponse) |
balance() | Joriy balansni qaytaradi (BalanceResponse) |
telegram() | TelegramChannelSender qaytaradi - ->send() va ->bindingLink() (telefonni botga bog‘lash havolasi) |
templates() | TemplatesClient qaytaradi - ->list() (foydalanish mumkin bo‘lgan shablonlar) |
reports() | ReportsClient qaytaradi - ->daily() / ->monthly() |
sendMessage() / registerPushDevice() / getReport() | @internal - to‘g‘ridan-to‘g‘ri chaqirilmaydi; yuqoridagi metodlar ularni ichida o‘zi chaqiradi |
use Illuminate\Support\Carbon;
$notify->reports()->daily(Carbon::today()->subDays(7), Carbon::today());
$notify->reports()->monthly();Rasm va formatlash
Telegram va WhatsApp xabarlariga ochiq JPEG/PNG rasm havolasini photoUrl orqali biriktiring - xabar matni rasm izohi bo'ladi (1024 belgidan uzun matn alohida xabar bo'lib ketadi). Telegram HTML'ni qo'llaydi (<b>, <i>, <u>, <a href>), WhatsApp esa *qalin*, _kursiv_, ~chizilgan~ belgilarini.
Notify::telegram()->send(
to: '+998901234567',
message: "<b>Buyurtmangiz tayyor</b>\nModel: DAF XF106",
photoUrl: 'https://example.com/photos/order.jpg',
);
// Laravel Notification
NotifyMessage::make()
->channel('whatsapp')
->message('*Buyurtmangiz tayyor*')
->photo('https://example.com/photos/order.jpg');WhatsApp shablonida (templateId) photoUrl shablonning IMAGE sarlavhasini to'ldiradi - shablon Meta'da rasmli sarlavha bilan tasdiqlangan bo'lishi kerak.
Qarz undirish
Qarzni topshiring - Notify qarzdorga undiruv strategiyasi bo'yicha kanalma-kanal eslatma yuboradi (masalan, muddatdan keyin 1-kuni Telegram, 2-kuni SMS, keyin WhatsApp va ovozli qo'ng'iroq), toki siz to'lovni xabar qilmaguningizcha. API client'da debts:manage scope bo'lishi kerak.
$debt = Notify::debts()->create(
name: 'Aziz Karimov',
phone: '+998901234567',
amount: 1250000,
dueDate: '2026-10-15',
externalId: 'INV-1001',
);
$debt->id; // "0f6c1a52-..."
$debt->nextReminderAt; // "2026-10-16T10:00:00+05:00"
$result = Notify::debts()->recordPayment($debt->id, 1250000, externalId: 'PAY-5001');
$result->debt->isPaid(); // true
$result->payment->attributedChannel; // "sms"
$result->payment->daysLate; // 2
Notify::debts()->find($debt->id)->payments;
Notify::debts()->cancel($debt->id);externalId- o'z hisob/shartnoma raqamingiz: qayta yuborilsa dublikat yaratilmaydi, shuning uchun faqat u berilganda avtomatik retry qilinadi.- Qisman to'lovlar qabul qilinadi - qoldiq uchun eslatmalar davom etadi. To'liq to'langach eslatmalar to'xtaydi.
attributedChannel- to'lovdan oldin yetkazilgan oxirgi eslatma kanali; panelda qaysi kanal samarali ekanligini ko'rsatadi.- Strategiya ID'si: panel -> Undiruv -> Strategiyalar -> API ID. Berilmasa, standart strategiya ishlatiladi.
- Xatolar:
EXCEEDS_REMAINING,DEBT_CLOSED,INVALID_AMOUNT,STRATEGY_NOT_FOUND(ValidationException).
Rejali xabarlar
Xabarni keyinroq yuboring - bir marta yoki takroriy jadval bo'yicha (har N soat/kunda, haftaning yoki oyning tanlangan kunlarida), so'ng uni o'zgartiring, to'xtating, davom ettiring yoki bekor qiling. Bu paneldagi "Rejali xabarlar" bilan bir xil rejalar. API client'da schedules:manage va kanalning yuborish scope'i (masalan, sms:send) bo'lishi kerak.
use CloudMe\Notify\ScheduledMessages\Recurrence;
$schedule = Notify::scheduledMessages()->create(
channel: 'sms',
startsAt: '2026-10-06T10:00:00+05:00',
recipient: '+998901234567',
message: 'Eslatma: bugun soat 15:00 da uchrashuv',
);
$schedule->id; // "7b1e0c4a-..."
$schedule->nextRunAt; // "2026-10-06T10:00:00+05:00"
Notify::scheduledMessages()->create(
channel: 'telegram',
startsAt: '2026-10-05T09:00:00+05:00',
contactGroupId: 12,
message: 'Salom, {{name}}! Haftalik yangiliklar...',
recurrence: Recurrence::weekly([1, 4]), // Recurrence::every(2, 'hours'), Recurrence::monthly([1, 15])
);
Notify::scheduledMessages()->update($schedule->id, channel: 'sms', startsAt: '2026-10-07T10:00:00+05:00', recipient: '+998901234567', message: 'Uchrashuv ertaga');
Notify::scheduledMessages()->pause($schedule->id);
Notify::scheduledMessages()->resume($schedule->id);
Notify::scheduledMessages()->cancel($schedule->id);
Notify::scheduledMessages()->list(status: 'active')->items;recurrenceberilmasa -startsAtda bir marta yuboriladi. Haftalik/oylik yuborishlarstartsAtdagi soatda bo'ladi.- Guruhga yuborish:
recipiento'rnigacontactGroupId(panel -> Kontaktlar -> Guruhlar -> API ID), matndagi{{name}}har bir kontakt ismi bilan almashtiriladi. Faqat production. update()butun rejani almashtiradi - barcha maydonlarni qayta bering.resume()keyingi navbatdagi vaqtdan davom etadi, o'tkazib yuborilganlar kechikib yuborilmaydi.- Har bir yuborish oddiy, pullik xabar (sandbox'da bepul). Muvaffaqiyatsiz yuborish (masalan, balans yetmasa)
lastErrorga yoziladi va reja keyingi vaqtga o'tadi. - Xatolar:
SCHEDULE_STATE_INVALID,SCHEDULE_START_PASSED,TEMPLATE_NOT_FOUND,CONTACT_GROUP_NOT_FOUND,SANDBOX_GROUP_UNSUPPORTED(ValidationException).
Webhook'larni qabul qilish
Webhook har bir API client uchun alohida ro'yxatdan o'tkaziladi (panel -> API clients -> Webhook). Har bir so'rov imzolanadi: X-Signature = "{X-Webhook-Timestamp}.{xom tana}" ning HMAC-SHA256'i. 5 daqiqadan eski so'rovlar rad etiladi, qayta urinishlarda X-Webhook-Id o'zgarmaydi.
.env ga imzo kalitini qo'ying va route'ni notify.webhook middleware bilan himoyalang (CSRF'dan ham chiqarib qo'ying):
// .env
NOTIFY_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxx
// routes/web.php
Route::post('/webhooks/notify', NotifyWebhookController::class)->middleware('notify.webhook');
// NotifyWebhookController
use CloudMe\NotifyLaravel\Http\Middleware\VerifyNotifyWebhook;
public function __invoke(Request $request)
{
$event = VerifyNotifyWebhook::event($request);
// $event->id, $event->event, $event->messageId, $event->status, $event->errorCode
if ($event->event === 'message.failed') {
// ...
}
return response()->noContent();
}Autentifikatsiya
Laravel SDK ishlatilganda quyidagilarning hech birini qo‘lda implement qilish shart emas - SDK ularni avtomatik boshqaradi:
- RSA so‘rov imzosi (request signing)
- timestamp va nonce generatsiyasi
- OAuth access token olish
- Token'ni cache'da saqlash
- Refresh token bilan avtomatik yangilash
- Tarmoq xatoliklarida qayta urinish (retry)
- Idempotency-Key generatsiyasi
RSA autentifikatsiyasi qanday ishlashini (masalan, HTTP orqali to‘g‘ridan-to‘g‘ri, SDK ishlatmasdan integratsiya qilmoqchi bo‘lgan dasturchilar uchun) REST API bo‘limidagi autentifikatsiya qismida batafsil o‘qishingiz mumkin - bu qism SDK ishlatmaydiganlar uchun saqlanib qolgan.
Xatoliklar bilan ishlash
cloudme/notify-php manba kodidagi haqiqiy tipdagi (typed) exception klasslari - hech qanday umumiy klass ichiga yashirilmagan:
use CloudMe\Notify\Exceptions\InsufficientBalanceException;
use CloudMe\Notify\Exceptions\ValidationException;
use CloudMe\Notify\Exceptions\RateLimitException;
try {
$response = $notify->sms()->send('+998901234567', 'Salom!');
} catch (InsufficientBalanceException $e) {
// $e->errorCode: INSUFFICIENT_BALANCE | SPENDING_LIMIT_EXCEEDED
// $e->hint() - nima qilish kerak, $e->docsUrl() - hujjatdagi shu xato (NOTIFY_LOCALE tilida)
// balans yetarli emas
} catch (ValidationException $e) {
$e->errors;
} catch (RateLimitException $e) {
$e->retryAfterSeconds;
}Boshqa mavjud klasslar (barchasi CloudMe\Notify\Exceptions namespace'ida): AuthenticationException, ForbiddenException, NotFoundException, ServerException, NetworkException, ApiException (boshqa hech biriga mos kelmagan holatlar uchun), va so‘rov hatto yuborilishidan oldin (masalan, noto‘g‘ri konfiguratsiya) tashlanadigan ConfigurationException.
5 daqiqalik Laravel Quick Start
composer require cloudme/notify-laravel.envga sandboxNOTIFY_CLIENT_ID/NOTIFY_API_KEYni kiriting- Xususiy kalitni
storage/app/notify/private.pemga joylashtiring vaNOTIFY_PRIVATE_KEY_PATHni shunga ko‘rsating php artisan notify:doctor- konfiguratsiya to‘g‘riligini tasdiqlang$notify->balance()- ulanish ishlayotganini tekshiring$notify->sms()->send(...)- sandbox SMS yuboring (real emas, bepul)$notify->message($response->messageId)- xabar holatini tekshiring
Shu 7 qadam bilan integratsiyangizni 5-10 daqiqa ichida tekshirib ko‘rishingiz mumkin.
Xavfsizlik bo‘yicha tavsiyalar
api_keyni frontend yoki mobil ilova kodiga qo‘ymang - u faqat backend serverda saqlanadi.- Xususiy kalit (
private.pem)public/papkasiga joylashtirilmasin. - Xususiy kalitni hech qachon Git repository'ga commit qilmang.
- Barcha credentiallar (
client_id,api_key, xususiy kalit) faqat backend serverda, environment sir sifatida saqlanadi. - Production va sandbox credentiallarini aralashtirib ishlatmang - server buni
ENVIRONMENT_MISMATCHxatosi bilan rad etadi. .env.examplefaylida hech qanday real sir bo‘lmasin - faqat bo‘sh/namuna qiymatlar.
Havolalar
cloudme/notify-laravel
Laravel иловаларингиз учун расмий интеграция - RSA имзолаш, OAuth token, retry ва Idempotency-Key автоматик бошқарилади.
composer require cloudme/notify-laravelЎрнатиш
Пакетни Composer орқали ўрнатинг:
composer require cloudme/notify-laravelLaravel'нинг package auto-discovery механизми cloudme/notify-laravelнинг composer.json файлидаги extra.laravel маълумотлари асосида NotifyServiceProviderни ва Notify facade'ини автоматик рўйхатдан ўтказади. config/app.phpга қўлда ҳеч нарса қўшиш шарт эмас.
Конфигурация
.env файлига қуйидагиларни қўшинг:
NOTIFY_ENVIRONMENT=sandbox
NOTIFY_CLIENT_ID=your_client_id
NOTIFY_API_KEY=your_api_key
NOTIFY_PRIVATE_KEY_PATH=storage/app/notify/private.pem
NOTIFY_TOKEN_STORE=cache
NOTIFY_LOCALE=uz-Cyrl- Хусусий калит серверда хавфсиз сақланиши керак (масалан,
storage/app/notify/-public/эмас). - Уни ҳеч қачон Git repository'га commit қилманг.
- Notify платформасида сизнинг хусусий калитингиз эмас, фақат унга мос очиқ (public) калит сақланади - сервер сизнинг имзонгизни шу очиқ калит билан текширади.
- Production сирларини (client_id, api_key, хусусий калит) ҳеч қачон документация ёки
.env.exampleичига ёзманг.
Sandbox ва Production
NOTIFY_ENVIRONMENT қиймати асосий URL'ни автоматик танлайди - уни қўлда ёзиш шарт эмас:
| Муҳит | Base URL |
|---|---|
| sandbox | https://sandbox.notify.cloudme.uz/api/v1 |
| production | https://api.notify.cloudme.uz/api/v1 |
Sandbox'да юборилган хабарлар учун нарх ҳисобланиб response'да қайтарилиши мумкин, лекин реал балансдан пул ечилмайди ва реал SMS/хабар юборилмайди. Notify сервери сизнинг client_id/api_keyингиз ҳақиқатан ҳам шу муҳитга тегишли эканини текширади - sandbox ва production credentialларини аралаштириб бўлмайди.
Диагностика: notify:doctor
Конфигурацияни реал хабар юбормасдан текшириш учун:
php artisan notify:doctorМуваффақиятли натижа намунаси:
Environment ........ sandbox
Base URL ........... https://sandbox.notify.cloudme.uz/api/v1
Client ID .......... configured
API key ............ configured
Private key file ... readable
Token store ........ cache
Cache store reachable yes
All checks passed. No messages were sent.SMS юбориш
Container'дан NotifyClientни олинг ва юборинг:
use CloudMe\Notify\NotifyClient;
$notify = app(NotifyClient::class);
$response = $notify->sms()->send(
'+998901234567',
'Salom! Bu CloudMe Notify orqali yuborilgan SMS.'
);Ёки Notify facade орқали:
use CloudMe\NotifyLaravel\Facades\Notify;
Notify::sms()->send(
'+998901234567',
'Salom! Bu CloudMe Notify orqali yuborilgan SMS.'
);$response - CloudMe\Notify\Responses\SendMessageResponse объекти:
$response->messageId;
$response->status;
$response->price;
$response->currency;
$response->balance;
$response->environment;send()нинг биринчи аргументи - string қабул қилувчи, массив эмас:
// Noto'g'ri - send()ning birinchi argumenti massiv emas, string:
$notify->sms()->send([
'to' => '+998901234567',
'message' => 'Salom!',
]);Хабар ҳолатини текшириш
$status = $notify->message($response->messageId);
$status->messageId;
$status->channel;
$status->recipient;
$status->status;
$status->smsType;
$status->price;
$status->currency;
$status->errorCode;
$status->createdAt;
$status->sentAt;
$status->deliveredAt;
$status->environment;Хабар ҳаётий сикли давомида status қуйидаги қийматлардан бирини олади (Notify REST API'даги ҳақиқий қийматлар):
createdmoderationqueuedprocessingsentdeliveredfailedrejectedcancelled
Балансни текшириш
$balance = $notify->balance();
$balance->balance;
$balance->reservedBalance;
$balance->currency;Шаблонсиз хабарлар модерацияси
Шаблон (templateId) билан юборилган хабар дарҳол кетади. Оддий матн тизим шаблонлари ва компаниянгиз шаблонлари билан солиштирилади: бирор шаблонга мос келса ({{ўзгарувчи}} ўрнида исталган қиймат бўлиши мумкин), у ҳам дарҳол юборилади.
Ҳеч бир шаблонга мос келмаган матн модерацияга тушади: status = moderation, маблағ ечилган, лекин хабар ҳали юборилмаган. Модератор тасдиқласа - queued, рад этса - rejected (маблағ қайтарилади, message.rejected webhook юборилади). Модератор шу матндан компаниянгиз учун шаблон яратиши мумкин - кейинги сафар бундай хабар модерациясиз кетади.
$response = Notify::sms()->send('+998901234567', 'Ёзги чегирма -50%!');
$response->status; // "moderation"
$response->isHeldForModeration(); // true
Notify::message($response->messageId)->isRejected();OTP - бир марталик код юбориш ва текшириш
Кодни платформа ўзи яратади, юборади ва текширади - иловангиз кодни кўрмайди ва сақламайди. Код узунлиги, амал қилиш муддати ва уринишлар сони Notify кабинетидаги компаниянгизнинг OTP созламаларидан, улар белгиланмаган бўлса - администратор белгилаган стандарт қийматлардан олинади. OTP оддий хабар нархида ҳисобланади ва модерацияга тушмайди.
use CloudMe\Notify\Exceptions\RateLimitException;
use CloudMe\NotifyLaravel\Facades\Notify;
// 1. Код юбориш - otp_id ни сақлаб қўйинг
try {
$otp = Notify::otp()->send(to: '+998901234567'); // channel: 'sms' | 'telegram' | 'whatsapp' | 'email'
} catch (RateLimitException $e) {
// OTP_RESEND_TOO_SOON - $e->retryAfterSeconds
}
session(['otp_id' => $otp->otpId]);
$otp->codeLength; // 6
$otp->expiresIn; // 300
$otp->maxAttempts; // 3
$otp->code; // production'да null, sandbox'да коднинг ўзи (тест учун)
// 2. Фойдаланувчи киритган кодни текшириш
$result = Notify::otp()->verify(session('otp_id'), $request->code);
if ($result->verified) {
// телефон тасдиқланди
} elseif ($result->canRetry()) {
// код нотўғри - $result->attemptsLeft та уриниш қолди
} else {
// муддати ўтган / уринишлар тугаган / ишлатилган - янги код юборинг
}- Нотўғри, муддати ўтган ёки ишлатилган код exception эмас, натижа сифатида қайтади. Номаълум
otp_id-NotFoundException. - Шу рақамга янги код юборилса, аввалгиси бекор бўлади. Жуда тез қайта сўралса -
RateLimitException(OTP_RESEND_TOO_SOON,retryAfterSeconds). send()ваverify()автоматик қайта юборилмайди: акс ҳолда иккинчи код кетиши ёки уриниш икки марта саналиши мумкин.- API client'да каналнинг юбориш scope'и бўлиши керак (масалан
sms:send).
REST API орқали
POST /api/v1/otp/send
{ "to": "998901234567", "channel": "sms" }
200 { "otp_id": "0f6c1a52-...", "status": "pending", "code_length": 6,
"max_attempts": 3, "expires_in": 300, "message_id": "...", "price": "120.0000" }
POST /api/v1/otp/verify
{ "otp_id": "0f6c1a52-...", "code": "481201" }
200 { "verified": true, "status": "verified" }
422 { "verified": false, "attempts_left": 2, "error": { "code": "OTP_INVALID" } }Бот ёки Firebase лойиҳасини танлаш
Каналлар бўлимидаги профил ID сини киритинг. FCM токенини рўйхатдан ўтказиш ва хабар юборишда бир хил лойиҳа ID сидан фойдаланинг. Ҳар бир ботда мижоз алоҳида боғланади. SDK 1.3 релизи учун.
$notify->telegram()->send(to: "998901234567", message: "Hello", channelAccountId: 7);
$notify->push()->registerDevice("998901234567", $fcmToken, channelAccountId: 12);
$notify->push()->send(to: "998901234567", message: "Hello", channelAccountId: 12);
NotifyMessage::make()->channel("telegram")->channelAccount(7)->message("Hello");Бошқа каналлар ва entry point'лар
Ҳар бир канал - ҳақиқий SDK манба кодидан текширилган, ишлайдиган мисоллар:
Telegram
$notify->telegram()->send('123456789', 'Buyurtmangiz tayyor.');Telegram телефон рақамига мижоз ботни бир марта очмагунча ёза олмайди. Аввал ҳаволани юборинг (7 кун амал қилади):
$binding = $notify->telegram()->bindingLink('+998901234567');
if (! $binding->bound) {
$notify->sms()->send('+998901234567', "Telegram: {$binding->link}");
}$notify->whatsapp()->send('+998901234567', 'Buyurtmangiz tayyor.');Voice (овозли қўнғироқ)
Voice канали send()нинг ўрнига ўқилиши қулайроқ call() алиасидан фойдаланади:
$notify->voice()->call('+998901234567', 'Sizning buyurtmangiz tayyor.');subject - send()нинг олтинчи (named) аргументи, шунинг учун named argument сифатида берилиши керак:
$notify->email()->send(
to: 'customer@example.com',
message: 'Buyurtmangiz tayyor.',
subject: 'Buyurtma holati'
);Push
$notify->push()->send(
to: '+998901234567',
message: 'Buyurtmangiz tayyor.',
subject: 'Buyurtma tayyor'
);Push юборишдан олдин мижознинг FCM device токенини рўйхатдан ўтказиш керак:
$notify->push()->registerDevice(
phone: $user->phone,
fcmToken: $request->fcm_token
);Шаблон (template) ва ўзгарувчилар
$notify->sms()->send(
to: '+998901234567',
templateId: 42,
variables: ['name' => 'Aziz', 'order_id' => '10293']
);Фойдаланиш мумкин бўлган шаблонлар рўйхати:
foreach ($notify->templates()->list('sms') as $template) {
$template->id; // templateId
$template->variables; // ['name', 'order_id']
}Idempotency-Key
Бир хил idempotencyKey билан такрорий сўров хабарни икки марта юбормайди - ўрнига биринчи натижа қайтарилади. Бермасангиз, SDK ўзи автоматик генерация қилади.
$notify->sms()->send(
to: '+998901234567',
message: 'Buyurtmangiz tayyor.',
idempotencyKey: (string) $order->id
);Барча entry point'лар
| Метод | Вазифаси |
|---|---|
sms() / telegram() / whatsapp() / voice() / email() | ChannelSender қайтаради - ->send() (voice учун ->call()) |
push() | PushChannelSender қайтаради - ->send() ва ->registerDevice() |
channel(string $channel) | Ҳали номланган методи йўқ канал учун умумий "escape hatch" - generic ChannelSender қайтаради |
message(string $messageId) | Битта хабарнинг ҳолатини қайтаради (MessageStatusResponse) |
balance() | Жорий балансни қайтаради (BalanceResponse) |
telegram() | TelegramChannelSender қайтаради - ->send() ва ->bindingLink() (телефонни ботга боғлаш ҳаволаси) |
templates() | TemplatesClient қайтаради - ->list() (фойдаланиш мумкин бўлган шаблонлар) |
reports() | ReportsClient қайтаради - ->daily() / ->monthly() |
sendMessage() / registerPushDevice() / getReport() | @internal - тўғридан-тўғри чақирилмайди; юқоридаги методлар уларни ичида ўзи чақиради |
use Illuminate\Support\Carbon;
$notify->reports()->daily(Carbon::today()->subDays(7), Carbon::today());
$notify->reports()->monthly();Расм ва форматлаш
Telegram ва WhatsApp хабарларига очиқ JPEG/PNG расм ҳаволасини photoUrl орқали бириктиринг - хабар матни расм изоҳи бўлади (1024 белгидан узун матн алоҳида хабар бўлиб кетади). Telegram HTML'ни қўллайди (<b>, <i>, <u>, <a href>), WhatsApp эса *қалин*, _курсив_, ~чизилган~ белгиларини.
Notify::telegram()->send(
to: '+998901234567',
message: "<b>Buyurtmangiz tayyor</b>\nModel: DAF XF106",
photoUrl: 'https://example.com/photos/order.jpg',
);
// Laravel Notification
NotifyMessage::make()
->channel('whatsapp')
->message('*Buyurtmangiz tayyor*')
->photo('https://example.com/photos/order.jpg');WhatsApp шаблонида (templateId) photoUrl шаблоннинг IMAGE сарлавҳасини тўлдиради - шаблон Meta'да расмли сарлавҳа билан тасдиқланган бўлиши керак.
Қарз ундириш
Қарзни топширинг - Notify қарздорга ундирув стратегияси бўйича каналма-канал эслатма юборади (масалан, муддатдан кейин 1-куни Telegram, 2-куни SMS, кейин WhatsApp ва овозли қўнғироқ), токи сиз тўловни хабар қилмагунингизча. API client'да debts:manage scope бўлиши керак.
$debt = Notify::debts()->create(
name: 'Aziz Karimov',
phone: '+998901234567',
amount: 1250000,
dueDate: '2026-10-15',
externalId: 'INV-1001',
);
$debt->id; // "0f6c1a52-..."
$debt->nextReminderAt; // "2026-10-16T10:00:00+05:00"
$result = Notify::debts()->recordPayment($debt->id, 1250000, externalId: 'PAY-5001');
$result->debt->isPaid(); // true
$result->payment->attributedChannel; // "sms"
$result->payment->daysLate; // 2
Notify::debts()->find($debt->id)->payments;
Notify::debts()->cancel($debt->id);externalId- ўз ҳисоб/шартнома рақамингиз: қайта юборилса дубликат яратилмайди, шунинг учун фақат у берилганда автоматик retry қилинади.- Қисман тўловлар қабул қилинади - қолдиқ учун эслатмалар давом этади. Тўлиқ тўлангач эслатмалар тўхтайди.
attributedChannel- тўловдан олдин етказилган охирги эслатма канали; панелда қайси канал самарали эканлигини кўрсатади.- Стратегия ID'си: панел -> Ундирув -> Стратегиялар -> API ID. Берилмаса, стандарт стратегия ишлатилади.
- Хатолар:
EXCEEDS_REMAINING,DEBT_CLOSED,INVALID_AMOUNT,STRATEGY_NOT_FOUND(ValidationException).
Режали хабарлар
Хабарни кейинроқ юборинг - бир марта ёки такрорий жадвал бўйича (ҳар N соат/кунда, ҳафтанинг ёки ойнинг танланган кунларида), сўнг уни ўзгартиринг, тўхтатинг, давом эттиринг ёки бекор қилинг. Бу панелдаги "Режали хабарлар" билан бир хил режалар. API client'да schedules:manage ва каналнинг юбориш scope'и (масалан, sms:send) бўлиши керак.
use CloudMe\Notify\ScheduledMessages\Recurrence;
$schedule = Notify::scheduledMessages()->create(
channel: 'sms',
startsAt: '2026-10-06T10:00:00+05:00',
recipient: '+998901234567',
message: 'Eslatma: bugun soat 15:00 da uchrashuv',
);
$schedule->id; // "7b1e0c4a-..."
$schedule->nextRunAt; // "2026-10-06T10:00:00+05:00"
Notify::scheduledMessages()->create(
channel: 'telegram',
startsAt: '2026-10-05T09:00:00+05:00',
contactGroupId: 12,
message: 'Salom, {{name}}! Haftalik yangiliklar...',
recurrence: Recurrence::weekly([1, 4]), // Recurrence::every(2, 'hours'), Recurrence::monthly([1, 15])
);
Notify::scheduledMessages()->update($schedule->id, channel: 'sms', startsAt: '2026-10-07T10:00:00+05:00', recipient: '+998901234567', message: 'Uchrashuv ertaga');
Notify::scheduledMessages()->pause($schedule->id);
Notify::scheduledMessages()->resume($schedule->id);
Notify::scheduledMessages()->cancel($schedule->id);
Notify::scheduledMessages()->list(status: 'active')->items;recurrenceберилмаса -startsAtда бир марта юборилади. Ҳафталик/ойлик юборишларstartsAtдаги соатда бўлади.- Гуруҳга юбориш:
recipientўрнигаcontactGroupId(панел -> Контактлар -> Гуруҳлар -> API ID), матндаги{{name}}ҳар бир контакт исми билан алмаштирилади. Фақат production. update()бутун режани алмаштиради - барча майдонларни қайта беринг.resume()кейинги навбатдаги вақтдан давом этади, ўтказиб юборилганлар кечикиб юборилмайди.- Ҳар бир юбориш оддий, пуллик хабар (sandbox'да бепул). Муваффақиятсиз юбориш (масалан, баланс етмаса)
lastErrorга ёзилади ва режа кейинги вақтга ўтади. - Хатолар:
SCHEDULE_STATE_INVALID,SCHEDULE_START_PASSED,TEMPLATE_NOT_FOUND,CONTACT_GROUP_NOT_FOUND,SANDBOX_GROUP_UNSUPPORTED(ValidationException).
Webhook'ларни қабул қилиш
Webhook ҳар бир API client учун алоҳида рўйхатдан ўтказилади (панел -> API clients -> Webhook). Ҳар бир сўров имзоланади: X-Signature = "{X-Webhook-Timestamp}.{хом тана}" нинг HMAC-SHA256'и. 5 дақиқадан эски сўровлар рад этилади, қайта уринишларда X-Webhook-Id ўзгармайди.
.env га имзо калитини қўйинг ва route'ни notify.webhook middleware билан ҳимояланг (CSRF'дан ҳам чиқариб қўйинг):
// .env
NOTIFY_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxx
// routes/web.php
Route::post('/webhooks/notify', NotifyWebhookController::class)->middleware('notify.webhook');
// NotifyWebhookController
use CloudMe\NotifyLaravel\Http\Middleware\VerifyNotifyWebhook;
public function __invoke(Request $request)
{
$event = VerifyNotifyWebhook::event($request);
// $event->id, $event->event, $event->messageId, $event->status, $event->errorCode
if ($event->event === 'message.failed') {
// ...
}
return response()->noContent();
}Аутентификация
Laravel SDK ишлатилганда қуйидагиларнинг ҳеч бирини қўлда implement қилиш шарт эмас - SDK уларни автоматик бошқаради:
- RSA сўров имзоси (request signing)
- timestamp ва nonce генерацияси
- OAuth access token олиш
- Token'ни cache'да сақлаш
- Refresh token билан автоматик янгилаш
- Тармоқ хатоликларида қайта уриниш (retry)
- Idempotency-Key генерацияси
RSA аутентификацияси қандай ишлашини (масалан, HTTP орқали тўғридан-тўғри, SDK ишлатмасдан интеграция қилмоқчи бўлган дастурчилар учун) REST API бўлимидаги аутентификация қисмида батафсил ўқишингиз мумкин - бу қисм SDK ишлатмайдиганлар учун сақланиб қолган.
Хатоликлар билан ишлаш
cloudme/notify-php манба кодидаги ҳақиқий типдаги (typed) exception класслари - ҳеч қандай умумий класс ичига яширилмаган:
use CloudMe\Notify\Exceptions\InsufficientBalanceException;
use CloudMe\Notify\Exceptions\ValidationException;
use CloudMe\Notify\Exceptions\RateLimitException;
try {
$response = $notify->sms()->send('+998901234567', 'Salom!');
} catch (InsufficientBalanceException $e) {
// $e->errorCode: INSUFFICIENT_BALANCE | SPENDING_LIMIT_EXCEEDED
// $e->hint() - нима қилиш керак, $e->docsUrl() - ҳужжатдаги шу хато (NOTIFY_LOCALE тилида)
// balans yetarli emas
} catch (ValidationException $e) {
$e->errors;
} catch (RateLimitException $e) {
$e->retryAfterSeconds;
}Бошқа мавжуд класслар (барчаси CloudMe\Notify\Exceptions namespace'ида): AuthenticationException, ForbiddenException, NotFoundException, ServerException, NetworkException, ApiException (бошқа ҳеч бирига мос келмаган ҳолатлар учун), ва сўров ҳатто юборилишидан олдин (масалан, нотўғри конфигурация) ташланадиган ConfigurationException.
5 дақиқалик Laravel Quick Start
composer require cloudme/notify-laravel.envга sandboxNOTIFY_CLIENT_ID/NOTIFY_API_KEYни киритинг- Хусусий калитни
storage/app/notify/private.pemга жойлаштиринг ваNOTIFY_PRIVATE_KEY_PATHни шунга кўрсатинг php artisan notify:doctor- конфигурация тўғрилигини тасдиқланг$notify->balance()- уланиш ишлаётганини текширинг$notify->sms()->send(...)- sandbox SMS юборинг (реал эмас, бепул)$notify->message($response->messageId)- хабар ҳолатини текширинг
Шу 7 қадам билан интеграциянгизни 5-10 дақиқа ичида текшириб кўришингиз мумкин.
Хавфсизлик бўйича тавсиялар
api_keyни frontend ёки мобил илова кодига қўйманг - у фақат backend серверда сақланади.- Хусусий калит (
private.pem)public/папкасига жойлаштирилмасин. - Хусусий калитни ҳеч қачон Git repository'га commit қилманг.
- Барча credentialлар (
client_id,api_key, хусусий калит) фақат backend серверда, environment сир сифатида сақланади. - Production ва sandbox credentialларини аралаштириб ишлатманг - сервер буни
ENVIRONMENT_MISMATCHхатоси билан рад этади. .env.exampleфайлида ҳеч қандай реал сир бўлмасин - фақат бўш/намуна қийматлар.
Ҳаволалар
cloudme/notify-laravel
Официальная интеграция для ваших Laravel-приложений - RSA-подпись, OAuth-токены, повторные попытки и Idempotency-Key управляются автоматически.
composer require cloudme/notify-laravelУстановка
Установите пакет через Composer:
composer require cloudme/notify-laravelМеханизм auto-discovery Laravel автоматически регистрирует NotifyServiceProvider и фасад Notify на основе секции extra.laravel в composer.json пакета cloudme/notify-laravel. Вручную ничего добавлять в config/app.php не нужно.
Конфигурация
Добавьте в файл .env:
NOTIFY_ENVIRONMENT=sandbox
NOTIFY_CLIENT_ID=your_client_id
NOTIFY_API_KEY=your_api_key
NOTIFY_PRIVATE_KEY_PATH=storage/app/notify/private.pem
NOTIFY_TOKEN_STORE=cache
NOTIFY_LOCALE=ru- Приватный ключ должен храниться на сервере в безопасном месте (например,
storage/app/notify/, а неpublic/). - Никогда не коммитьте его в Git-репозиторий.
- На платформе Notify хранится не ваш приватный ключ, а только соответствующий ему публичный ключ - сервер проверяет вашу подпись именно этим публичным ключом.
- Никогда не вставляйте production-секреты (client_id, api_key, приватный ключ) в примеры документации или в
.env.example.
Sandbox и Production
Значение NOTIFY_ENVIRONMENT автоматически выбирает базовый URL - указывать его вручную не нужно:
| Окружение | Base URL |
|---|---|
| sandbox | https://sandbox.notify.cloudme.uz/api/v1 |
| production | https://api.notify.cloudme.uz/api/v1 |
В sandbox-окружении цена сообщения может быть рассчитана и возвращена в ответе, но реальные средства с баланса не списываются, а реальное сообщение не отправляется. Сервер Notify проверяет, что ваши client_id/api_key действительно принадлежат указанному окружению - смешивать sandbox- и production-учётные данные нельзя.
Диагностика: notify:doctor
Проверка конфигурации без отправки реального сообщения:
php artisan notify:doctorПример успешного результата:
Environment ........ sandbox
Base URL ........... https://sandbox.notify.cloudme.uz/api/v1
Client ID .......... configured
API key ............ configured
Private key file ... readable
Token store ........ cache
Cache store reachable yes
All checks passed. No messages were sent.Отправка SMS
Получите NotifyClient из контейнера и отправьте:
use CloudMe\Notify\NotifyClient;
$notify = app(NotifyClient::class);
$response = $notify->sms()->send(
'+998901234567',
'Salom! Bu CloudMe Notify orqali yuborilgan SMS.'
);Или через фасад Notify:
use CloudMe\NotifyLaravel\Facades\Notify;
Notify::sms()->send(
'+998901234567',
'Salom! Bu CloudMe Notify orqali yuborilgan SMS.'
);$response - объект CloudMe\Notify\Responses\SendMessageResponse:
$response->messageId;
$response->status;
$response->price;
$response->currency;
$response->balance;
$response->environment;send() - это строка, а не массив:
// Neverno - pervyy argument send() eto stroka, a ne massiv:
$notify->sms()->send([
'to' => '+998901234567',
'message' => 'Salom!',
]);Проверка статуса сообщения
$status = $notify->message($response->messageId);
$status->messageId;
$status->channel;
$status->recipient;
$status->status;
$status->smsType;
$status->price;
$status->currency;
$status->errorCode;
$status->createdAt;
$status->sentAt;
$status->deliveredAt;
$status->environment;В течение жизненного цикла сообщения status принимает одно из следующих значений (реальные значения из Notify REST API):
createdmoderationqueuedprocessingsentdeliveredfailedrejectedcancelled
Проверка баланса
$balance = $notify->balance();
$balance->balance;
$balance->reservedBalance;
$balance->currency;Модерация сообщений без шаблона
Сообщение, отправленное с шаблоном (templateId), уходит сразу. Обычный текст сравнивается с системными шаблонами и шаблонами вашей компании: если он подходит под шаблон (на месте {{переменной}} может быть любое значение), он тоже уходит сразу.
Текст, не подошедший ни под один шаблон, попадает на модерацию: status = moderation, средства списаны, но сообщение ещё не отправлено. После одобрения - queued, при отклонении - rejected (средства возвращаются, отправляется webhook message.rejected). Модератор может создать из этого текста шаблон для вашей компании - тогда похожие сообщения пойдут без модерации.
$response = Notify::sms()->send('+998901234567', 'Летняя скидка -50%!');
$response->status; // "moderation"
$response->isHeldForModeration(); // true
Notify::message($response->messageId)->isRejected();OTP - отправка и проверка одноразового кода
Платформа сама генерирует, отправляет и проверяет код - ваше приложение не видит и не хранит его. Длина кода, срок действия и число попыток берутся из OTP-настроек вашей компании в кабинете Notify, а если они не заданы - из значений по умолчанию, заданных администратором. OTP тарифицируется как обычное сообщение и не попадает на модерацию.
use CloudMe\Notify\Exceptions\RateLimitException;
use CloudMe\NotifyLaravel\Facades\Notify;
// 1. Отправка кода - сохраните otp_id
try {
$otp = Notify::otp()->send(to: '+998901234567'); // channel: 'sms' | 'telegram' | 'whatsapp' | 'email'
} catch (RateLimitException $e) {
// OTP_RESEND_TOO_SOON - $e->retryAfterSeconds
}
session(['otp_id' => $otp->otpId]);
$otp->codeLength; // 6
$otp->expiresIn; // 300
$otp->maxAttempts; // 3
$otp->code; // в production - null, в sandbox - сам код (для тестов)
// 2. Проверка кода, введённого пользователем
$result = Notify::otp()->verify(session('otp_id'), $request->code);
if ($result->verified) {
// телефон подтверждён
} elseif ($result->canRetry()) {
// неверный код - осталось $result->attemptsLeft попыток
} else {
// истёк / попытки исчерпаны / уже использован - отправьте новый код
}- Неверный, истёкший или использованный код возвращается как результат, а не исключение. Неизвестный
otp_id-NotFoundException. - Новый код на тот же номер отменяет предыдущий. Слишком частый запрос -
RateLimitException(OTP_RESEND_TOO_SOON,retryAfterSeconds). send()иverify()не повторяются автоматически: иначе может уйти второй код или попытка засчитается дважды.- У API-клиента должен быть scope отправки по каналу (например
sms:send).
Через REST API
POST /api/v1/otp/send
{ "to": "998901234567", "channel": "sms" }
200 { "otp_id": "0f6c1a52-...", "status": "pending", "code_length": 6,
"max_attempts": 3, "expires_in": 300, "message_id": "...", "price": "120.0000" }
POST /api/v1/otp/verify
{ "otp_id": "0f6c1a52-...", "code": "481201" }
200 { "verified": true, "status": "verified" }
422 { "verified": false, "attempts_left": 2, "error": { "code": "OTP_INVALID" } }Выбор бота или проекта Firebase
Укажите ID профиля из раздела Каналы. Используйте один ID проекта при регистрации FCM-токена и отправке. Получатель привязывается отдельно к каждому боту. Для релиза SDK 1.3.
$notify->telegram()->send(to: "998901234567", message: "Hello", channelAccountId: 7);
$notify->push()->registerDevice("998901234567", $fcmToken, channelAccountId: 12);
$notify->push()->send(to: "998901234567", message: "Hello", channelAccountId: 12);
NotifyMessage::make()->channel("telegram")->channelAccount(7)->message("Hello");Другие каналы и точки входа
Каждый канал - рабочий пример, проверенный по реальному исходному коду SDK:
Telegram
$notify->telegram()->send('123456789', 'Buyurtmangiz tayyor.');Telegram не может написать на номер, пока клиент хотя бы раз не открыл бота. Сначала отправьте ссылку (действует 7 дней):
$binding = $notify->telegram()->bindingLink('+998901234567');
if (! $binding->bound) {
$notify->sms()->send('+998901234567', "Telegram: {$binding->link}");
}$notify->whatsapp()->send('+998901234567', 'Buyurtmangiz tayyor.');Voice (голосовой звонок)
Канал voice использует более удобный для чтения алиас call() вместо send():
$notify->voice()->call('+998901234567', 'Sizning buyurtmangiz tayyor.');subject - шестой (именованный) аргумент send(), поэтому его нужно передавать как именованный аргумент:
$notify->email()->send(
to: 'customer@example.com',
message: 'Buyurtmangiz tayyor.',
subject: 'Buyurtma holati'
);Push
$notify->push()->send(
to: '+998901234567',
message: 'Buyurtmangiz tayyor.',
subject: 'Buyurtma tayyor'
);Перед отправкой push нужно зарегистрировать FCM-токен устройства клиента:
$notify->push()->registerDevice(
phone: $user->phone,
fcmToken: $request->fcm_token
);Шаблон (template) и переменные
$notify->sms()->send(
to: '+998901234567',
templateId: 42,
variables: ['name' => 'Aziz', 'order_id' => '10293']
);Список доступных шаблонов:
foreach ($notify->templates()->list('sms') as $template) {
$template->id; // templateId
$template->variables; // ['name', 'order_id']
}Idempotency-Key
Повторный запрос с тем же idempotencyKey не отправляет сообщение дважды - возвращается первоначальный результат. Если не указать ключ, SDK сгенерирует его автоматически.
$notify->sms()->send(
to: '+998901234567',
message: 'Buyurtmangiz tayyor.',
idempotencyKey: (string) $order->id
);Все точки входа
| Метод | Назначение |
|---|---|
sms() / telegram() / whatsapp() / voice() / email() | Возвращает ChannelSender - ->send() (для voice - ->call()) |
push() | Возвращает PushChannelSender - ->send() и ->registerDevice() |
channel(string $channel) | "Запасной вариант" для канала, у которого ещё нет именованного метода - возвращает обычный ChannelSender |
message(string $messageId) | Возвращает статус одного сообщения (MessageStatusResponse) |
balance() | Возвращает текущий баланс (BalanceResponse) |
telegram() | Возвращает TelegramChannelSender - ->send() и ->bindingLink() (ссылка для привязки телефона к боту) |
templates() | Возвращает TemplatesClient - ->list() (доступные шаблоны) |
reports() | Возвращает ReportsClient - ->daily() / ->monthly() |
sendMessage() / registerPushDevice() / getReport() | @internal - не вызываются напрямую; вызываются изнутри перечисленными выше методами |
use Illuminate\Support\Carbon;
$notify->reports()->daily(Carbon::today()->subDays(7), Carbon::today());
$notify->reports()->monthly();Фото и форматирование
К сообщениям Telegram и WhatsApp можно прикрепить публичную ссылку на JPEG/PNG через photoUrl - текст станет подписью к фото (текст длиннее 1024 символов уходит отдельным сообщением). Telegram поддерживает HTML (<b>, <i>, <u>, <a href>), WhatsApp - разметку *жирный*, _курсив_, ~зачёркнутый~.
Notify::telegram()->send(
to: '+998901234567',
message: "<b>Buyurtmangiz tayyor</b>\nModel: DAF XF106",
photoUrl: 'https://example.com/photos/order.jpg',
);
// Laravel Notification
NotifyMessage::make()
->channel('whatsapp')
->message('*Buyurtmangiz tayyor*')
->photo('https://example.com/photos/order.jpg');В шаблоне WhatsApp (templateId) photoUrl заполняет IMAGE-заголовок - шаблон должен быть одобрен Meta с таким заголовком.
Взыскание долгов
Передайте долг - Notify будет напоминать должнику по каналам по очереди согласно стратегии взыскания (например, Telegram на 1-й день после срока, SMS на 2-й, затем WhatsApp и голосовой звонок), пока вы не сообщите об оплате. Нужен scope debts:manage.
$debt = Notify::debts()->create(
name: 'Aziz Karimov',
phone: '+998901234567',
amount: 1250000,
dueDate: '2026-10-15',
externalId: 'INV-1001',
);
$debt->id; // "0f6c1a52-..."
$debt->nextReminderAt; // "2026-10-16T10:00:00+05:00"
$result = Notify::debts()->recordPayment($debt->id, 1250000, externalId: 'PAY-5001');
$result->debt->isPaid(); // true
$result->payment->attributedChannel; // "sms"
$result->payment->daysLate; // 2
Notify::debts()->find($debt->id)->payments;
Notify::debts()->cancel($debt->id);externalId- ваш номер счёта/договора: повторная отправка не создаёт дубликат, поэтому автоматический retry выполняется только при его наличии.- Частичные платежи принимаются - напоминания продолжаются на остаток. После полной оплаты напоминания прекращаются.
attributedChannel- канал последнего доставленного перед платежом напоминания; в панели видно, какой канал эффективен.- ID стратегии: панель -> Взыскание -> Стратегии -> API ID. Если не указан - используется стратегия по умолчанию.
- Ошибки:
EXCEEDS_REMAINING,DEBT_CLOSED,INVALID_AMOUNT,STRATEGY_NOT_FOUND(ValidationException).
Запланированные сообщения
Отправьте сообщение позже - разово или по повторяющемуся расписанию (каждые N часов/дней, в выбранные дни недели или месяца), затем изменяйте, приостанавливайте, возобновляйте или отменяйте его. Это те же расписания, что и в разделе панели "Запланированные сообщения". Нужен scope schedules:manage и scope отправки канала (например, sms:send).
use CloudMe\Notify\ScheduledMessages\Recurrence;
$schedule = Notify::scheduledMessages()->create(
channel: 'sms',
startsAt: '2026-10-06T10:00:00+05:00',
recipient: '+998901234567',
message: 'Eslatma: bugun soat 15:00 da uchrashuv',
);
$schedule->id; // "7b1e0c4a-..."
$schedule->nextRunAt; // "2026-10-06T10:00:00+05:00"
Notify::scheduledMessages()->create(
channel: 'telegram',
startsAt: '2026-10-05T09:00:00+05:00',
contactGroupId: 12,
message: 'Salom, {{name}}! Haftalik yangiliklar...',
recurrence: Recurrence::weekly([1, 4]), // Recurrence::every(2, 'hours'), Recurrence::monthly([1, 15])
);
Notify::scheduledMessages()->update($schedule->id, channel: 'sms', startsAt: '2026-10-07T10:00:00+05:00', recipient: '+998901234567', message: 'Uchrashuv ertaga');
Notify::scheduledMessages()->pause($schedule->id);
Notify::scheduledMessages()->resume($schedule->id);
Notify::scheduledMessages()->cancel($schedule->id);
Notify::scheduledMessages()->list(status: 'active')->items;- Без
recurrenceсообщение отправляется один раз вstartsAt. Еженедельные/ежемесячные отправки идут во время суток изstartsAt. - Отправка группе:
contactGroupIdвместоrecipient(панель -> Контакты -> Группы -> API ID),{{name}}в тексте заменяется именем каждого контакта. Только production. update()заменяет расписание целиком - передайте все поля заново.resume()продолжает со следующего срока, пропущенные отправки не досылаются.- Каждая отправка - обычное платное сообщение (в sandbox бесплатно). Неудачная отправка (например, нехватка средств) записывается в
lastError, и расписание переходит к следующему сроку. - Ошибки:
SCHEDULE_STATE_INVALID,SCHEDULE_START_PASSED,TEMPLATE_NOT_FOUND,CONTACT_GROUP_NOT_FOUND,SANDBOX_GROUP_UNSUPPORTED(ValidationException).
Приём вебхуков
Вебхук регистрируется отдельно для каждого API-клиента (панель -> API клиенты -> Webhook). Каждый запрос подписан: X-Signature = HMAC-SHA256 от "{X-Webhook-Timestamp}.{сырое тело}". Запросы старше 5 минут отклоняются, при повторах X-Webhook-Id не меняется.
Укажите секрет подписи в .env и защитите маршрут middleware notify.webhook (и исключите его из проверки CSRF):
// .env
NOTIFY_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxx
// routes/web.php
Route::post('/webhooks/notify', NotifyWebhookController::class)->middleware('notify.webhook');
// NotifyWebhookController
use CloudMe\NotifyLaravel\Http\Middleware\VerifyNotifyWebhook;
public function __invoke(Request $request)
{
$event = VerifyNotifyWebhook::event($request);
// $event->id, $event->event, $event->messageId, $event->status, $event->errorCode
if ($event->event === 'message.failed') {
// ...
}
return response()->noContent();
}Аутентификация
При использовании Laravel SDK ничего из перечисленного не нужно реализовывать вручную - SDK делает это автоматически:
- RSA-подпись запроса (request signing)
- Генерация timestamp и nonce
- Получение OAuth access token
- Кеширование токена
- Автоматическое обновление по refresh token
- Повторные попытки при сетевых ошибках (retry)
- Генерация Idempotency-Key
Как именно работает RSA-аутентификация (например, для разработчиков, интегрирующихся напрямую по HTTP, без SDK), подробно описано в разделе аутентификации на вкладке REST API - этот раздел сохранён специально для тех, кто не использует SDK.
Обработка ошибок
Реальные типизированные классы исключений из исходного кода cloudme/notify-php - ничего не скрыто за общим классом:
use CloudMe\Notify\Exceptions\InsufficientBalanceException;
use CloudMe\Notify\Exceptions\ValidationException;
use CloudMe\Notify\Exceptions\RateLimitException;
try {
$response = $notify->sms()->send('+998901234567', 'Salom!');
} catch (InsufficientBalanceException $e) {
// $e->errorCode: INSUFFICIENT_BALANCE | SPENDING_LIMIT_EXCEEDED
// $e->hint() - что сделать, $e->docsUrl() - эта ошибка в документации (на языке NOTIFY_LOCALE)
// nedostatochno balansa
} catch (ValidationException $e) {
$e->errors;
} catch (RateLimitException $e) {
$e->retryAfterSeconds;
}Другие доступные классы (все в пространстве имён CloudMe\Notify\Exceptions): AuthenticationException, ForbiddenException, NotFoundException, ServerException, NetworkException, ApiException (запасной вариант для остальных случаев) и ConfigurationException, которое выбрасывается ещё до отправки запроса (например, при неверной конфигурации).
5-минутный Laravel Quick Start
composer require cloudme/notify-laravel- Укажите в
.envsandbox-значенияNOTIFY_CLIENT_ID/NOTIFY_API_KEY - Поместите приватный ключ в
storage/app/notify/private.pemи укажите его вNOTIFY_PRIVATE_KEY_PATH php artisan notify:doctor- убедитесь, что конфигурация верна$notify->balance()- проверьте, что соединение работает$notify->sms()->send(...)- отправьте sandbox-SMS (не реальное, бесплатно)$notify->message($response->messageId)- проверьте статус сообщения
Эти 7 шагов позволяют проверить интеграцию за 5-10 минут.
Рекомендации по безопасности
- Не помещайте
api_keyво фронтенд или код мобильного приложения - он хранится только на backend-сервере. - Приватный ключ (
private.pem) не должен находиться в папкеpublic/. - Никогда не коммитьте приватный ключ в Git-репозиторий.
- Все учётные данные (
client_id,api_key, приватный ключ) хранятся только на backend-сервере как переменные окружения. - Не смешивайте production- и sandbox-учётные данные - сервер отклонит это с ошибкой
ENVIRONMENT_MISMATCH. - В файле
.env.exampleне должно быть реальных секретов - только пустые/примерные значения.
Ссылки
cloudme/notify-laravel
The official integration for your Laravel applications - RSA signing, OAuth tokens, retries, and Idempotency-Key generation are all handled automatically.
composer require cloudme/notify-laravelInstallation
Install the package via Composer:
composer require cloudme/notify-laravelLaravel's package auto-discovery automatically registers NotifyServiceProvider and the Notify facade based on the extra.laravel section of cloudme/notify-laravel's composer.json. There is nothing to add to config/app.php by hand.
Configuration
Add the following to your .env file:
NOTIFY_ENVIRONMENT=sandbox
NOTIFY_CLIENT_ID=your_client_id
NOTIFY_API_KEY=your_api_key
NOTIFY_PRIVATE_KEY_PATH=storage/app/notify/private.pem
NOTIFY_TOKEN_STORE=cache
NOTIFY_LOCALE=en- It must be stored securely on the server (e.g.
storage/app/notify/, neverpublic/). - Never commit it to a Git repository.
- The Notify platform never stores your private key - only the matching public key is stored there, and that's what the server uses to verify your signature.
- Never paste production secrets (client_id, api_key, private key) into documentation examples or into
.env.example.
Sandbox and Production
NOTIFY_ENVIRONMENT alone picks the base URL automatically - you never write it by hand:
| Environment | Base URL |
|---|---|
| sandbox | https://sandbox.notify.cloudme.uz/api/v1 |
| production | https://api.notify.cloudme.uz/api/v1 |
In sandbox, a message's price may be calculated and returned in the response, but no real funds are ever deducted from your balance and no real message is ever delivered. Notify's server verifies that your client_id/api_key actually belong to the environment you're calling - sandbox and production credentials can never be mixed.
Diagnostics: notify:doctor
Check your configuration without sending a real message:
php artisan notify:doctorExample successful output:
Environment ........ sandbox
Base URL ........... https://sandbox.notify.cloudme.uz/api/v1
Client ID .......... configured
API key ............ configured
Private key file ... readable
Token store ........ cache
Cache store reachable yes
All checks passed. No messages were sent.Sending an SMS
Resolve NotifyClient from the container and send:
use CloudMe\Notify\NotifyClient;
$notify = app(NotifyClient::class);
$response = $notify->sms()->send(
'+998901234567',
'Salom! Bu CloudMe Notify orqali yuborilgan SMS.'
);Or via the Notify facade:
use CloudMe\NotifyLaravel\Facades\Notify;
Notify::sms()->send(
'+998901234567',
'Salom! Bu CloudMe Notify orqali yuborilgan SMS.'
);$response is a CloudMe\Notify\Responses\SendMessageResponse:
$response->messageId;
$response->status;
$response->price;
$response->currency;
$response->balance;
$response->environment;send() is a string, not an array:
// Wrong - send()'s first argument is a string, not an array:
$notify->sms()->send([
'to' => '+998901234567',
'message' => 'Salom!',
]);Checking message status
$status = $notify->message($response->messageId);
$status->messageId;
$status->channel;
$status->recipient;
$status->status;
$status->smsType;
$status->price;
$status->currency;
$status->errorCode;
$status->createdAt;
$status->sentAt;
$status->deliveredAt;
$status->environment;Over its lifecycle, status takes one of the following values (the real values from the Notify REST API):
createdmoderationqueuedprocessingsentdeliveredfailedrejectedcancelled
Checking balance
$balance = $notify->balance();
$balance->balance;
$balance->reservedBalance;
$balance->currency;Moderation of free-text messages
A message sent with a template (templateId) goes out immediately. Plain text is compared with the system templates and your company's templates: if it fits one (a {{variable}} may hold any value), it goes out immediately too.
Text that fits no template is held for moderation: status = moderation, already charged but not sent yet. Approved - queued; rejected - rejected (refunded, a message.rejected webhook is sent). A moderator can turn the text into a template for your company, so similar messages go out without moderation next time.
$response = Notify::sms()->send('+998901234567', 'Summer sale -50%!');
$response->status; // "moderation"
$response->isHeldForModeration(); // true
Notify::message($response->messageId)->isRejected();OTP - send and verify one-time codes
The platform generates, delivers and verifies the code - your app never sees or stores it. Code length, validity and allowed attempts come from your company's OTP settings in the Notify cabinet, or the defaults set by the administrator when you have not set your own. OTP messages are billed like normal messages and are never held for moderation.
use CloudMe\Notify\Exceptions\RateLimitException;
use CloudMe\NotifyLaravel\Facades\Notify;
// 1. Send a code - keep the otp_id
try {
$otp = Notify::otp()->send(to: '+998901234567'); // channel: 'sms' | 'telegram' | 'whatsapp' | 'email'
} catch (RateLimitException $e) {
// OTP_RESEND_TOO_SOON - $e->retryAfterSeconds
}
session(['otp_id' => $otp->otpId]);
$otp->codeLength; // 6
$otp->expiresIn; // 300
$otp->maxAttempts; // 3
$otp->code; // null in production; the code itself in the sandbox (for testing)
// 2. Verify what the user typed
$result = Notify::otp()->verify(session('otp_id'), $request->code);
if ($result->verified) {
// phone confirmed
} elseif ($result->canRetry()) {
// wrong code - $result->attemptsLeft tries left
} else {
// expired / attempts used up / already used - send a new code
}- A wrong, expired or used code is returned as a result, not thrown. An unknown
otp_idthrowsNotFoundException. - A new code for the same number cancels the previous one. Asking again too soon throws
RateLimitException(OTP_RESEND_TOO_SOON,retryAfterSeconds). send()andverify()are never retried automatically - a retry could deliver a second code or count as two attempts.- Your API client needs the channel's send scope (e.g.
sms:send).
Via the REST API
POST /api/v1/otp/send
{ "to": "998901234567", "channel": "sms" }
200 { "otp_id": "0f6c1a52-...", "status": "pending", "code_length": 6,
"max_attempts": 3, "expires_in": 300, "message_id": "...", "price": "120.0000" }
POST /api/v1/otp/verify
{ "otp_id": "0f6c1a52-...", "code": "481201" }
200 { "verified": true, "status": "verified" }
422 { "verified": false, "attempts_left": 2, "error": { "code": "OTP_INVALID" } }Choose a bot or Firebase project
Use the profile ID from Channels. Register the FCM token and send using the same project ID. Each bot requires a separate recipient binding. For SDK release 1.3.
$notify->telegram()->send(to: "998901234567", message: "Hello", channelAccountId: 7);
$notify->push()->registerDevice("998901234567", $fcmToken, channelAccountId: 12);
$notify->push()->send(to: "998901234567", message: "Hello", channelAccountId: 12);
NotifyMessage::make()->channel("telegram")->channelAccount(7)->message("Hello");Other channels and entry points
Every channel below is a working example, verified directly against the SDK's real source code:
Telegram
$notify->telegram()->send('123456789', 'Buyurtmangiz tayyor.');Telegram cannot message a phone number until its owner has opened your bot once. Send the link first (valid for 7 days):
$binding = $notify->telegram()->bindingLink('+998901234567');
if (! $binding->bound) {
$notify->sms()->send('+998901234567', "Telegram: {$binding->link}");
}$notify->whatsapp()->send('+998901234567', 'Buyurtmangiz tayyor.');Voice
The voice channel uses the more natural-reading call() alias instead of send():
$notify->voice()->call('+998901234567', 'Sizning buyurtmangiz tayyor.');subject is send()'s sixth parameter, so it must be passed as a named argument:
$notify->email()->send(
to: 'customer@example.com',
message: 'Buyurtmangiz tayyor.',
subject: 'Buyurtma holati'
);Push
$notify->push()->send(
to: '+998901234567',
message: 'Buyurtmangiz tayyor.',
subject: 'Buyurtma tayyor'
);Before sending a push, register the customer's FCM device token:
$notify->push()->registerDevice(
phone: $user->phone,
fcmToken: $request->fcm_token
);Templates and variables
$notify->sms()->send(
to: '+998901234567',
templateId: 42,
variables: ['name' => 'Aziz', 'order_id' => '10293']
);The templates you can use:
foreach ($notify->templates()->list('sms') as $template) {
$template->id; // templateId
$template->variables; // ['name', 'order_id']
}Idempotency-Key
A retried request with the same idempotencyKey never sends the message twice - the original result is returned instead. If you don't supply one, the SDK generates it for you automatically.
$notify->sms()->send(
to: '+998901234567',
message: 'Buyurtmangiz tayyor.',
idempotencyKey: (string) $order->id
);All entry points
| Method | Purpose |
|---|---|
sms() / telegram() / whatsapp() / voice() / email() | Returns a ChannelSender - ->send() (->call() for voice) |
push() | Returns a PushChannelSender - ->send() and ->registerDevice() |
channel(string $channel) | Escape hatch for a channel without a named method yet - returns a generic ChannelSender |
message(string $messageId) | Returns one message's status (MessageStatusResponse) |
balance() | Returns the current balance (BalanceResponse) |
telegram() | Returns TelegramChannelSender - ->send() and ->bindingLink() (a link that binds a phone to the bot) |
templates() | Returns TemplatesClient - ->list() (templates you can use) |
reports() | Returns a ReportsClient - ->daily() / ->monthly() |
sendMessage() / registerPushDevice() / getReport() | @internal - never called directly; the methods above call them internally |
use Illuminate\Support\Carbon;
$notify->reports()->daily(Carbon::today()->subDays(7), Carbon::today());
$notify->reports()->monthly();Photos and formatting
Attach a public JPEG/PNG URL to Telegram and WhatsApp messages with photoUrl - the text becomes the photo's caption (text over 1024 characters follows as a separate message). Telegram renders HTML (<b>, <i>, <u>, <a href>); WhatsApp uses *bold*, _italic_, ~strike~.
Notify::telegram()->send(
to: '+998901234567',
message: "<b>Buyurtmangiz tayyor</b>\nModel: DAF XF106",
photoUrl: 'https://example.com/photos/order.jpg',
);
// Laravel Notification
NotifyMessage::make()
->channel('whatsapp')
->message('*Buyurtmangiz tayyor*')
->photo('https://example.com/photos/order.jpg');With a WhatsApp template (templateId), photoUrl fills the template's IMAGE header - the template must be approved by Meta with one.
Debt collection
Hand a debt over and Notify reminds the debtor channel by channel following a collection strategy (e.g. Telegram on day 1 after the due date, SMS on day 2, then WhatsApp and a voice call) until you report it paid. Requires the debts:manage scope.
$debt = Notify::debts()->create(
name: 'Aziz Karimov',
phone: '+998901234567',
amount: 1250000,
dueDate: '2026-10-15',
externalId: 'INV-1001',
);
$debt->id; // "0f6c1a52-..."
$debt->nextReminderAt; // "2026-10-16T10:00:00+05:00"
$result = Notify::debts()->recordPayment($debt->id, 1250000, externalId: 'PAY-5001');
$result->debt->isPaid(); // true
$result->payment->attributedChannel; // "sms"
$result->payment->daysLate; // 2
Notify::debts()->find($debt->id)->payments;
Notify::debts()->cancel($debt->id);externalId- your own invoice/contract reference: resending it never creates a duplicate, so calls are only retried automatically when it is set.- Partial payments are accepted - reminders continue for the remainder and stop once the debt is fully paid.
attributedChannel- the last delivered reminder before the payment; the dashboard uses it to show which channel works.- Strategy ID: dashboard -> Collection -> Strategies -> API ID. Omit it to use your default strategy.
- Errors:
EXCEEDS_REMAINING,DEBT_CLOSED,INVALID_AMOUNT,STRATEGY_NOT_FOUND(ValidationException).
Scheduled messages
Send a message later - once or on a recurring timetable (every N hours/days, on chosen weekdays or days of the month) - then edit, pause, resume or cancel it. These are the same schedules as the dashboard's Scheduled messages page. Requires the schedules:manage scope plus the channel's send scope (e.g. sms:send).
use CloudMe\Notify\ScheduledMessages\Recurrence;
$schedule = Notify::scheduledMessages()->create(
channel: 'sms',
startsAt: '2026-10-06T10:00:00+05:00',
recipient: '+998901234567',
message: 'Eslatma: bugun soat 15:00 da uchrashuv',
);
$schedule->id; // "7b1e0c4a-..."
$schedule->nextRunAt; // "2026-10-06T10:00:00+05:00"
Notify::scheduledMessages()->create(
channel: 'telegram',
startsAt: '2026-10-05T09:00:00+05:00',
contactGroupId: 12,
message: 'Salom, {{name}}! Haftalik yangiliklar...',
recurrence: Recurrence::weekly([1, 4]), // Recurrence::every(2, 'hours'), Recurrence::monthly([1, 15])
);
Notify::scheduledMessages()->update($schedule->id, channel: 'sms', startsAt: '2026-10-07T10:00:00+05:00', recipient: '+998901234567', message: 'Uchrashuv ertaga');
Notify::scheduledMessages()->pause($schedule->id);
Notify::scheduledMessages()->resume($schedule->id);
Notify::scheduledMessages()->cancel($schedule->id);
Notify::scheduledMessages()->list(status: 'active')->items;- Without
recurrencethe message is sent once atstartsAt. Weekly/monthly sends happen at the time of day ofstartsAt. - Groups: pass
contactGroupId(dashboard -> Contacts -> Groups -> API ID) instead ofrecipient;{{name}}in the text becomes each contact's name. Production only. update()replaces the whole schedule - pass every field again.resume()continues at the next occurrence; sends missed while paused are skipped.- Each send is a normal, billed message (free in sandbox). A failed send (e.g. insufficient balance) is recorded in
lastErrorand the schedule moves on to its next occurrence. - Errors:
SCHEDULE_STATE_INVALID,SCHEDULE_START_PASSED,TEMPLATE_NOT_FOUND,CONTACT_GROUP_NOT_FOUND,SANDBOX_GROUP_UNSUPPORTED(ValidationException).
Receiving webhooks
Webhooks are registered per API client (dashboard -> API clients -> Webhook). Every request is signed: X-Signature is the HMAC-SHA256 of "{X-Webhook-Timestamp}.{raw body}". Requests older than 5 minutes are rejected, and X-Webhook-Id stays the same across retries.
Put the signing secret in .env and protect the route with the notify.webhook middleware (exclude it from CSRF too):
// .env
NOTIFY_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxx
// routes/web.php
Route::post('/webhooks/notify', NotifyWebhookController::class)->middleware('notify.webhook');
// NotifyWebhookController
use CloudMe\NotifyLaravel\Http\Middleware\VerifyNotifyWebhook;
public function __invoke(Request $request)
{
$event = VerifyNotifyWebhook::event($request);
// $event->id, $event->event, $event->messageId, $event->status, $event->errorCode
if ($event->event === 'message.failed') {
// ...
}
return response()->noContent();
}Authentication
When using the Laravel SDK, you never have to implement any of the following by hand - the SDK handles them all automatically:
- RSA request signing
- Timestamp and nonce generation
- Obtaining an OAuth access token
- Caching the token
- Automatic refresh via the refresh token
- Retrying on network errors
- Idempotency-Key generation
How RSA authentication actually works (for developers integrating directly over HTTP without the SDK) is documented in detail in the authentication section of the REST API tab - that section is kept specifically for developers who aren't using the SDK.
Error handling
Real, typed exception classes from cloudme/notify-php's source - none of them hidden behind a generic wrapper:
use CloudMe\Notify\Exceptions\InsufficientBalanceException;
use CloudMe\Notify\Exceptions\ValidationException;
use CloudMe\Notify\Exceptions\RateLimitException;
try {
$response = $notify->sms()->send('+998901234567', 'Salom!');
} catch (InsufficientBalanceException $e) {
// $e->errorCode: INSUFFICIENT_BALANCE | SPENDING_LIMIT_EXCEEDED
// $e->hint() - what to do next, $e->docsUrl() - this error in the docs (in the NOTIFY_LOCALE language)
// balance too low
} catch (ValidationException $e) {
$e->errors;
} catch (RateLimitException $e) {
$e->retryAfterSeconds;
}Also available (all under the CloudMe\Notify\Exceptions namespace): AuthenticationException, ForbiddenException, NotFoundException, ServerException, NetworkException, ApiException (fallback for anything else), and ConfigurationException, thrown before a request is even attempted (e.g. bad configuration).
5-minute Laravel Quick Start
composer require cloudme/notify-laravel- Add your sandbox
NOTIFY_CLIENT_ID/NOTIFY_API_KEYto.env - Place your private key at
storage/app/notify/private.pemand pointNOTIFY_PRIVATE_KEY_PATHat it php artisan notify:doctor- confirm your configuration is correct$notify->balance()- confirm connectivity$notify->sms()->send(...)- send a sandbox SMS (not real, free)$notify->message($response->messageId)- check its status
These 7 steps let you verify your integration in 5-10 minutes.
Security best practices
- Never put
api_keyinto frontend or mobile app code - it belongs on the backend server only. - Your private key (
private.pem) must never live underpublic/. - Never commit the private key to a Git repository.
- Every credential (
client_id,api_key, private key) is stored only on the backend server, as an environment secret. - Never mix production and sandbox credentials - the server rejects that with
ENVIRONMENT_MISMATCH. .env.examplemust never contain a real secret - only blank/placeholder values.
Links
cloudme/notify-php
Freymvorkdan mustaqil, asosiy PHP SDK - Laravel loyihalar uchun uning ustiga qurilgan tavsiya etiladi.
composer require cloudme/notify-phpBot yoki Firebase loyihasini tanlash
Kanallar bo‘limidagi profil ID sini kiriting. FCM tokenini ro‘yxatdan o‘tkazish va xabar yuborishda bir xil loyiha ID sidan foydalaning. Har bir botda mijoz alohida bog‘lanadi. SDK 1.3 relizi uchun.
$notify->telegram()->send(to: "998901234567", message: "Hello", channelAccountId: 7);
$notify->push()->registerDevice("998901234567", $fcmToken, channelAccountId: 12);
$notify->push()->send(to: "998901234567", message: "Hello", channelAccountId: 12);Tezkor boshlash
use CloudMe\Notify\NotifyClient;
$notify = new NotifyClient(
clientId: getenv('NOTIFY_CLIENT_ID'),
apiKey: getenv('NOTIFY_API_KEY'),
privateKey: '/secure/path/private.pem',
baseUrl: 'https://sandbox.notify.cloudme.uz/api/v1',
locale: 'uz', // ixtiyoriy: xato matnlari tili - uz, uz-Cyrl, ru, en
);
$response = $notify->sms()->send('+998901234567', 'Salom!');RSA imzolash, OAuth token, retry, Idempotency-Key va barcha kanallar - xuddi Laravel SDK'dagidek, chunki cloudme/notify-laravel aynan shu paket ustiga qurilgan.
OTP (bir martalik kod)
Kodni platforma o'zi yaratadi, yuboradi va tekshiradi - ilovangiz kodni ko'rmaydi va saqlamaydi. Kod uzunligi, amal qilish muddati va urinishlar soni Notify kabinetidagi kompaniyangizning OTP sozlamalaridan, ular belgilanmagan bo'lsa - administrator belgilagan standart qiymatlardan olinadi. OTP oddiy xabar narxida hisoblanadi va moderatsiyaga tushmaydi.
// 1. Kod yuborish - otp_id ni saqlab qo'ying
$otp = $notify->otp()->send(to: '+998901234567');
// 2. Foydalanuvchi kiritgan kodni tekshirish
$result = $notify->otp()->verify($otp->otpId, $codeFromUser);
$result->verified; // true | false
$result->errorCode; // OTP_INVALID | OTP_EXPIRED | OTP_ATTEMPTS_EXCEEDED | OTP_ALREADY_VERIFIED
$result->attemptsLeft;
$result->canRetry();Shablonsiz matn hech bir shablonga mos kelmasa, moderatsiyaga tushadi ($response->isHeldForModeration()). OTP va tezkor xabarlar uchun shablon yoki OTP API'dan foydalaning.
Rasm va formatlash
Telegram va WhatsApp xabarlariga ochiq JPEG/PNG rasm havolasini photoUrl orqali biriktiring - xabar matni rasm izohi bo'ladi (1024 belgidan uzun matn alohida xabar bo'lib ketadi). Telegram HTML'ni qo'llaydi (<b>, <i>, <u>, <a href>), WhatsApp esa *qalin*, _kursiv_, ~chizilgan~ belgilarini.
$notify->telegram()->send(
to: '+998901234567',
message: "<b>Buyurtmangiz tayyor</b>\nModel: DAF XF106",
photoUrl: 'https://example.com/photos/order.jpg',
);
$notify->whatsapp()->send(to: '+998901234567', message: '*Buyurtmangiz tayyor*', photoUrl: 'https://example.com/photos/order.jpg');WhatsApp shablonida (templateId) photoUrl shablonning IMAGE sarlavhasini to'ldiradi - shablon Meta'da rasmli sarlavha bilan tasdiqlangan bo'lishi kerak.
Qarz undirish
Qarzni topshiring - Notify qarzdorga undiruv strategiyasi bo'yicha kanalma-kanal eslatma yuboradi (masalan, muddatdan keyin 1-kuni Telegram, 2-kuni SMS, keyin WhatsApp va ovozli qo'ng'iroq), toki siz to'lovni xabar qilmaguningizcha. API client'da debts:manage scope bo'lishi kerak.
$debt = $notify->debts()->create(
name: 'Aziz Karimov',
phone: '+998901234567',
amount: 1250000,
dueDate: '2026-10-15',
externalId: 'INV-1001',
);
$debt->id; // "0f6c1a52-..."
$debt->nextReminderAt; // "2026-10-16T10:00:00+05:00"
$result = $notify->debts()->recordPayment($debt->id, 1250000, externalId: 'PAY-5001');
$result->debt->isPaid(); // true
$result->payment->attributedChannel; // "sms"
$result->payment->daysLate; // 2
$notify->debts()->find($debt->id)->payments;
$notify->debts()->cancel($debt->id);externalId- o'z hisob/shartnoma raqamingiz: qayta yuborilsa dublikat yaratilmaydi, shuning uchun faqat u berilganda avtomatik retry qilinadi.- Qisman to'lovlar qabul qilinadi - qoldiq uchun eslatmalar davom etadi. To'liq to'langach eslatmalar to'xtaydi.
attributedChannel- to'lovdan oldin yetkazilgan oxirgi eslatma kanali; panelda qaysi kanal samarali ekanligini ko'rsatadi.- Strategiya ID'si: panel -> Undiruv -> Strategiyalar -> API ID. Berilmasa, standart strategiya ishlatiladi.
- Xatolar:
EXCEEDS_REMAINING,DEBT_CLOSED,INVALID_AMOUNT,STRATEGY_NOT_FOUND(ValidationException).
Rejali xabarlar
Xabarni keyinroq yuboring - bir marta yoki takroriy jadval bo'yicha (har N soat/kunda, haftaning yoki oyning tanlangan kunlarida), so'ng uni o'zgartiring, to'xtating, davom ettiring yoki bekor qiling. Bu paneldagi "Rejali xabarlar" bilan bir xil rejalar. API client'da schedules:manage va kanalning yuborish scope'i (masalan, sms:send) bo'lishi kerak.
use CloudMe\Notify\ScheduledMessages\Recurrence;
$schedule = $notify->scheduledMessages()->create(
channel: 'sms',
startsAt: '2026-10-06T10:00:00+05:00',
recipient: '+998901234567',
message: 'Eslatma: bugun soat 15:00 da uchrashuv',
);
$schedule->id; // "7b1e0c4a-..."
$schedule->nextRunAt; // "2026-10-06T10:00:00+05:00"
$notify->scheduledMessages()->create(
channel: 'telegram',
startsAt: '2026-10-05T09:00:00+05:00',
contactGroupId: 12,
message: 'Salom, {{name}}! Haftalik yangiliklar...',
recurrence: Recurrence::weekly([1, 4]), // Recurrence::every(2, 'hours'), Recurrence::monthly([1, 15])
);
$notify->scheduledMessages()->update($schedule->id, channel: 'sms', startsAt: '2026-10-07T10:00:00+05:00', recipient: '+998901234567', message: 'Uchrashuv ertaga');
$notify->scheduledMessages()->pause($schedule->id);
$notify->scheduledMessages()->resume($schedule->id);
$notify->scheduledMessages()->cancel($schedule->id);
$notify->scheduledMessages()->list(status: 'active')->items;recurrenceberilmasa -startsAtda bir marta yuboriladi. Haftalik/oylik yuborishlarstartsAtdagi soatda bo'ladi.- Guruhga yuborish:
recipiento'rnigacontactGroupId(panel -> Kontaktlar -> Guruhlar -> API ID), matndagi{{name}}har bir kontakt ismi bilan almashtiriladi. Faqat production. update()butun rejani almashtiradi - barcha maydonlarni qayta bering.resume()keyingi navbatdagi vaqtdan davom etadi, o'tkazib yuborilganlar kechikib yuborilmaydi.- Har bir yuborish oddiy, pullik xabar (sandbox'da bepul). Muvaffaqiyatsiz yuborish (masalan, balans yetmasa)
lastErrorga yoziladi va reja keyingi vaqtga o'tadi. - Xatolar:
SCHEDULE_STATE_INVALID,SCHEDULE_START_PASSED,TEMPLATE_NOT_FOUND,CONTACT_GROUP_NOT_FOUND,SANDBOX_GROUP_UNSUPPORTED(ValidationException).
Webhook'larni qabul qilish
Webhook har bir API client uchun alohida ro'yxatdan o'tkaziladi (panel -> API clients -> Webhook). Har bir so'rov imzolanadi: X-Signature = "{X-Webhook-Timestamp}.{xom tana}" ning HMAC-SHA256'i. 5 daqiqadan eski so'rovlar rad etiladi, qayta urinishlarda X-Webhook-Id o'zgarmaydi.
Har doim so'rovning xom tanasini bering - qayta kodlangan massiv imzoni buzadi:
use CloudMe\Notify\Exceptions\InvalidWebhookSignatureException;
use CloudMe\Notify\Webhooks\WebhookVerifier;
$verifier = new WebhookVerifier(getenv('NOTIFY_WEBHOOK_SECRET'));
try {
$event = $verifier->parse(file_get_contents('php://input'), getallheaders());
} catch (InvalidWebhookSignatureException) {
http_response_code(401);
exit;
}
// $event->id, $event->event, $event->messageId, $event->status, $event->errorCode
http_response_code(200);Havolalar
cloudme/notify-php
Фреймворкдан мустақил, асосий PHP SDK - Laravel лойиҳалар учун унинг устига қурилган тавсия этилади.
composer require cloudme/notify-phpБот ёки Firebase лойиҳасини танлаш
Каналлар бўлимидаги профил ID сини киритинг. FCM токенини рўйхатдан ўтказиш ва хабар юборишда бир хил лойиҳа ID сидан фойдаланинг. Ҳар бир ботда мижоз алоҳида боғланади. SDK 1.3 релизи учун.
$notify->telegram()->send(to: "998901234567", message: "Hello", channelAccountId: 7);
$notify->push()->registerDevice("998901234567", $fcmToken, channelAccountId: 12);
$notify->push()->send(to: "998901234567", message: "Hello", channelAccountId: 12);Тезкор бошлаш
use CloudMe\Notify\NotifyClient;
$notify = new NotifyClient(
clientId: getenv('NOTIFY_CLIENT_ID'),
apiKey: getenv('NOTIFY_API_KEY'),
privateKey: '/secure/path/private.pem',
baseUrl: 'https://sandbox.notify.cloudme.uz/api/v1',
locale: 'uz-Cyrl', // ихтиёрий: хато матнлари тили - uz, uz-Cyrl, ru, en
);
$response = $notify->sms()->send('+998901234567', 'Salom!');RSA имзолаш, OAuth token, retry, Idempotency-Key ва барча каналлар - худди Laravel SDK'дагидек, чунки cloudme/notify-laravel айнан шу пакет устига қурилган.
OTP (бир марталик код)
Кодни платформа ўзи яратади, юборади ва текширади - иловангиз кодни кўрмайди ва сақламайди. Код узунлиги, амал қилиш муддати ва уринишлар сони Notify кабинетидаги компаниянгизнинг OTP созламаларидан, улар белгиланмаган бўлса - администратор белгилаган стандарт қийматлардан олинади. OTP оддий хабар нархида ҳисобланади ва модерацияга тушмайди.
// 1. Код юбориш - otp_id ни сақлаб қўйинг
$otp = $notify->otp()->send(to: '+998901234567');
// 2. Фойдаланувчи киритган кодни текшириш
$result = $notify->otp()->verify($otp->otpId, $codeFromUser);
$result->verified; // true | false
$result->errorCode; // OTP_INVALID | OTP_EXPIRED | OTP_ATTEMPTS_EXCEEDED | OTP_ALREADY_VERIFIED
$result->attemptsLeft;
$result->canRetry();Шаблонсиз матн ҳеч бир шаблонга мос келмаса, модерацияга тушади ($response->isHeldForModeration()). OTP ва тезкор хабарлар учун шаблон ёки OTP API'дан фойдаланинг.
Расм ва форматлаш
Telegram ва WhatsApp хабарларига очиқ JPEG/PNG расм ҳаволасини photoUrl орқали бириктиринг - хабар матни расм изоҳи бўлади (1024 белгидан узун матн алоҳида хабар бўлиб кетади). Telegram HTML'ни қўллайди (<b>, <i>, <u>, <a href>), WhatsApp эса *қалин*, _курсив_, ~чизилган~ белгиларини.
$notify->telegram()->send(
to: '+998901234567',
message: "<b>Buyurtmangiz tayyor</b>\nModel: DAF XF106",
photoUrl: 'https://example.com/photos/order.jpg',
);
$notify->whatsapp()->send(to: '+998901234567', message: '*Buyurtmangiz tayyor*', photoUrl: 'https://example.com/photos/order.jpg');WhatsApp шаблонида (templateId) photoUrl шаблоннинг IMAGE сарлавҳасини тўлдиради - шаблон Meta'да расмли сарлавҳа билан тасдиқланган бўлиши керак.
Қарз ундириш
Қарзни топширинг - Notify қарздорга ундирув стратегияси бўйича каналма-канал эслатма юборади (масалан, муддатдан кейин 1-куни Telegram, 2-куни SMS, кейин WhatsApp ва овозли қўнғироқ), токи сиз тўловни хабар қилмагунингизча. API client'да debts:manage scope бўлиши керак.
$debt = $notify->debts()->create(
name: 'Aziz Karimov',
phone: '+998901234567',
amount: 1250000,
dueDate: '2026-10-15',
externalId: 'INV-1001',
);
$debt->id; // "0f6c1a52-..."
$debt->nextReminderAt; // "2026-10-16T10:00:00+05:00"
$result = $notify->debts()->recordPayment($debt->id, 1250000, externalId: 'PAY-5001');
$result->debt->isPaid(); // true
$result->payment->attributedChannel; // "sms"
$result->payment->daysLate; // 2
$notify->debts()->find($debt->id)->payments;
$notify->debts()->cancel($debt->id);externalId- ўз ҳисоб/шартнома рақамингиз: қайта юборилса дубликат яратилмайди, шунинг учун фақат у берилганда автоматик retry қилинади.- Қисман тўловлар қабул қилинади - қолдиқ учун эслатмалар давом этади. Тўлиқ тўлангач эслатмалар тўхтайди.
attributedChannel- тўловдан олдин етказилган охирги эслатма канали; панелда қайси канал самарали эканлигини кўрсатади.- Стратегия ID'си: панел -> Ундирув -> Стратегиялар -> API ID. Берилмаса, стандарт стратегия ишлатилади.
- Хатолар:
EXCEEDS_REMAINING,DEBT_CLOSED,INVALID_AMOUNT,STRATEGY_NOT_FOUND(ValidationException).
Режали хабарлар
Хабарни кейинроқ юборинг - бир марта ёки такрорий жадвал бўйича (ҳар N соат/кунда, ҳафтанинг ёки ойнинг танланган кунларида), сўнг уни ўзгартиринг, тўхтатинг, давом эттиринг ёки бекор қилинг. Бу панелдаги "Режали хабарлар" билан бир хил режалар. API client'да schedules:manage ва каналнинг юбориш scope'и (масалан, sms:send) бўлиши керак.
use CloudMe\Notify\ScheduledMessages\Recurrence;
$schedule = $notify->scheduledMessages()->create(
channel: 'sms',
startsAt: '2026-10-06T10:00:00+05:00',
recipient: '+998901234567',
message: 'Eslatma: bugun soat 15:00 da uchrashuv',
);
$schedule->id; // "7b1e0c4a-..."
$schedule->nextRunAt; // "2026-10-06T10:00:00+05:00"
$notify->scheduledMessages()->create(
channel: 'telegram',
startsAt: '2026-10-05T09:00:00+05:00',
contactGroupId: 12,
message: 'Salom, {{name}}! Haftalik yangiliklar...',
recurrence: Recurrence::weekly([1, 4]), // Recurrence::every(2, 'hours'), Recurrence::monthly([1, 15])
);
$notify->scheduledMessages()->update($schedule->id, channel: 'sms', startsAt: '2026-10-07T10:00:00+05:00', recipient: '+998901234567', message: 'Uchrashuv ertaga');
$notify->scheduledMessages()->pause($schedule->id);
$notify->scheduledMessages()->resume($schedule->id);
$notify->scheduledMessages()->cancel($schedule->id);
$notify->scheduledMessages()->list(status: 'active')->items;recurrenceберилмаса -startsAtда бир марта юборилади. Ҳафталик/ойлик юборишларstartsAtдаги соатда бўлади.- Гуруҳга юбориш:
recipientўрнигаcontactGroupId(панел -> Контактлар -> Гуруҳлар -> API ID), матндаги{{name}}ҳар бир контакт исми билан алмаштирилади. Фақат production. update()бутун режани алмаштиради - барча майдонларни қайта беринг.resume()кейинги навбатдаги вақтдан давом этади, ўтказиб юборилганлар кечикиб юборилмайди.- Ҳар бир юбориш оддий, пуллик хабар (sandbox'да бепул). Муваффақиятсиз юбориш (масалан, баланс етмаса)
lastErrorга ёзилади ва режа кейинги вақтга ўтади. - Хатолар:
SCHEDULE_STATE_INVALID,SCHEDULE_START_PASSED,TEMPLATE_NOT_FOUND,CONTACT_GROUP_NOT_FOUND,SANDBOX_GROUP_UNSUPPORTED(ValidationException).
Webhook'ларни қабул қилиш
Webhook ҳар бир API client учун алоҳида рўйхатдан ўтказилади (панел -> API clients -> Webhook). Ҳар бир сўров имзоланади: X-Signature = "{X-Webhook-Timestamp}.{хом тана}" нинг HMAC-SHA256'и. 5 дақиқадан эски сўровлар рад этилади, қайта уринишларда X-Webhook-Id ўзгармайди.
Ҳар доим сўровнинг хом танасини беринг - қайта кодланган массив имзони бузади:
use CloudMe\Notify\Exceptions\InvalidWebhookSignatureException;
use CloudMe\Notify\Webhooks\WebhookVerifier;
$verifier = new WebhookVerifier(getenv('NOTIFY_WEBHOOK_SECRET'));
try {
$event = $verifier->parse(file_get_contents('php://input'), getallheaders());
} catch (InvalidWebhookSignatureException) {
http_response_code(401);
exit;
}
// $event->id, $event->event, $event->messageId, $event->status, $event->errorCode
http_response_code(200);Ҳаволалар
cloudme/notify-php
Базовый SDK, независимый от фреймворка - для Laravel-проектов рекомендуется построенный поверх него .
composer require cloudme/notify-phpВыбор бота или проекта Firebase
Укажите ID профиля из раздела Каналы. Используйте один ID проекта при регистрации FCM-токена и отправке. Получатель привязывается отдельно к каждому боту. Для релиза SDK 1.3.
$notify->telegram()->send(to: "998901234567", message: "Hello", channelAccountId: 7);
$notify->push()->registerDevice("998901234567", $fcmToken, channelAccountId: 12);
$notify->push()->send(to: "998901234567", message: "Hello", channelAccountId: 12);Быстрый старт
use CloudMe\Notify\NotifyClient;
$notify = new NotifyClient(
clientId: getenv('NOTIFY_CLIENT_ID'),
apiKey: getenv('NOTIFY_API_KEY'),
privateKey: '/secure/path/private.pem',
baseUrl: 'https://sandbox.notify.cloudme.uz/api/v1',
locale: 'ru', // необязательно: язык текстов ошибок - uz, uz-Cyrl, ru, en
);
$response = $notify->sms()->send('+998901234567', 'Salom!');RSA-подпись, OAuth-токен, retry, Idempotency-Key и все каналы - точно так же, как в Laravel SDK, поскольку cloudme/notify-laravel построен именно поверх этого пакета.
OTP (одноразовый код)
Платформа сама генерирует, отправляет и проверяет код - ваше приложение не видит и не хранит его. Длина кода, срок действия и число попыток берутся из OTP-настроек вашей компании в кабинете Notify, а если они не заданы - из значений по умолчанию, заданных администратором. OTP тарифицируется как обычное сообщение и не попадает на модерацию.
// 1. Отправка кода - сохраните otp_id
$otp = $notify->otp()->send(to: '+998901234567');
// 2. Проверка кода, введённого пользователем
$result = $notify->otp()->verify($otp->otpId, $codeFromUser);
$result->verified; // true | false
$result->errorCode; // OTP_INVALID | OTP_EXPIRED | OTP_ATTEMPTS_EXCEEDED | OTP_ALREADY_VERIFIED
$result->attemptsLeft;
$result->canRetry();Текст без шаблона, не подошедший ни под один шаблон, попадает на модерацию ($response->isHeldForModeration()). Для OTP и срочных сообщений используйте шаблон или OTP API.
Фото и форматирование
К сообщениям Telegram и WhatsApp можно прикрепить публичную ссылку на JPEG/PNG через photoUrl - текст станет подписью к фото (текст длиннее 1024 символов уходит отдельным сообщением). Telegram поддерживает HTML (<b>, <i>, <u>, <a href>), WhatsApp - разметку *жирный*, _курсив_, ~зачёркнутый~.
$notify->telegram()->send(
to: '+998901234567',
message: "<b>Buyurtmangiz tayyor</b>\nModel: DAF XF106",
photoUrl: 'https://example.com/photos/order.jpg',
);
$notify->whatsapp()->send(to: '+998901234567', message: '*Buyurtmangiz tayyor*', photoUrl: 'https://example.com/photos/order.jpg');В шаблоне WhatsApp (templateId) photoUrl заполняет IMAGE-заголовок - шаблон должен быть одобрен Meta с таким заголовком.
Взыскание долгов
Передайте долг - Notify будет напоминать должнику по каналам по очереди согласно стратегии взыскания (например, Telegram на 1-й день после срока, SMS на 2-й, затем WhatsApp и голосовой звонок), пока вы не сообщите об оплате. Нужен scope debts:manage.
$debt = $notify->debts()->create(
name: 'Aziz Karimov',
phone: '+998901234567',
amount: 1250000,
dueDate: '2026-10-15',
externalId: 'INV-1001',
);
$debt->id; // "0f6c1a52-..."
$debt->nextReminderAt; // "2026-10-16T10:00:00+05:00"
$result = $notify->debts()->recordPayment($debt->id, 1250000, externalId: 'PAY-5001');
$result->debt->isPaid(); // true
$result->payment->attributedChannel; // "sms"
$result->payment->daysLate; // 2
$notify->debts()->find($debt->id)->payments;
$notify->debts()->cancel($debt->id);externalId- ваш номер счёта/договора: повторная отправка не создаёт дубликат, поэтому автоматический retry выполняется только при его наличии.- Частичные платежи принимаются - напоминания продолжаются на остаток. После полной оплаты напоминания прекращаются.
attributedChannel- канал последнего доставленного перед платежом напоминания; в панели видно, какой канал эффективен.- ID стратегии: панель -> Взыскание -> Стратегии -> API ID. Если не указан - используется стратегия по умолчанию.
- Ошибки:
EXCEEDS_REMAINING,DEBT_CLOSED,INVALID_AMOUNT,STRATEGY_NOT_FOUND(ValidationException).
Запланированные сообщения
Отправьте сообщение позже - разово или по повторяющемуся расписанию (каждые N часов/дней, в выбранные дни недели или месяца), затем изменяйте, приостанавливайте, возобновляйте или отменяйте его. Это те же расписания, что и в разделе панели "Запланированные сообщения". Нужен scope schedules:manage и scope отправки канала (например, sms:send).
use CloudMe\Notify\ScheduledMessages\Recurrence;
$schedule = $notify->scheduledMessages()->create(
channel: 'sms',
startsAt: '2026-10-06T10:00:00+05:00',
recipient: '+998901234567',
message: 'Eslatma: bugun soat 15:00 da uchrashuv',
);
$schedule->id; // "7b1e0c4a-..."
$schedule->nextRunAt; // "2026-10-06T10:00:00+05:00"
$notify->scheduledMessages()->create(
channel: 'telegram',
startsAt: '2026-10-05T09:00:00+05:00',
contactGroupId: 12,
message: 'Salom, {{name}}! Haftalik yangiliklar...',
recurrence: Recurrence::weekly([1, 4]), // Recurrence::every(2, 'hours'), Recurrence::monthly([1, 15])
);
$notify->scheduledMessages()->update($schedule->id, channel: 'sms', startsAt: '2026-10-07T10:00:00+05:00', recipient: '+998901234567', message: 'Uchrashuv ertaga');
$notify->scheduledMessages()->pause($schedule->id);
$notify->scheduledMessages()->resume($schedule->id);
$notify->scheduledMessages()->cancel($schedule->id);
$notify->scheduledMessages()->list(status: 'active')->items;- Без
recurrenceсообщение отправляется один раз вstartsAt. Еженедельные/ежемесячные отправки идут во время суток изstartsAt. - Отправка группе:
contactGroupIdвместоrecipient(панель -> Контакты -> Группы -> API ID),{{name}}в тексте заменяется именем каждого контакта. Только production. update()заменяет расписание целиком - передайте все поля заново.resume()продолжает со следующего срока, пропущенные отправки не досылаются.- Каждая отправка - обычное платное сообщение (в sandbox бесплатно). Неудачная отправка (например, нехватка средств) записывается в
lastError, и расписание переходит к следующему сроку. - Ошибки:
SCHEDULE_STATE_INVALID,SCHEDULE_START_PASSED,TEMPLATE_NOT_FOUND,CONTACT_GROUP_NOT_FOUND,SANDBOX_GROUP_UNSUPPORTED(ValidationException).
Приём вебхуков
Вебхук регистрируется отдельно для каждого API-клиента (панель -> API клиенты -> Webhook). Каждый запрос подписан: X-Signature = HMAC-SHA256 от "{X-Webhook-Timestamp}.{сырое тело}". Запросы старше 5 минут отклоняются, при повторах X-Webhook-Id не меняется.
Всегда передавайте сырое тело запроса - повторно закодированный массив ломает подпись:
use CloudMe\Notify\Exceptions\InvalidWebhookSignatureException;
use CloudMe\Notify\Webhooks\WebhookVerifier;
$verifier = new WebhookVerifier(getenv('NOTIFY_WEBHOOK_SECRET'));
try {
$event = $verifier->parse(file_get_contents('php://input'), getallheaders());
} catch (InvalidWebhookSignatureException) {
http_response_code(401);
exit;
}
// $event->id, $event->event, $event->messageId, $event->status, $event->errorCode
http_response_code(200);Ссылки
cloudme/notify-php
The framework-independent core SDK - for Laravel projects, the package built on top of it is recommended instead.
composer require cloudme/notify-phpChoose a bot or Firebase project
Use the profile ID from Channels. Register the FCM token and send using the same project ID. Each bot requires a separate recipient binding. For SDK release 1.3.
$notify->telegram()->send(to: "998901234567", message: "Hello", channelAccountId: 7);
$notify->push()->registerDevice("998901234567", $fcmToken, channelAccountId: 12);
$notify->push()->send(to: "998901234567", message: "Hello", channelAccountId: 12);Quick start
use CloudMe\Notify\NotifyClient;
$notify = new NotifyClient(
clientId: getenv('NOTIFY_CLIENT_ID'),
apiKey: getenv('NOTIFY_API_KEY'),
privateKey: '/secure/path/private.pem',
baseUrl: 'https://sandbox.notify.cloudme.uz/api/v1',
locale: 'en', // optional: language of error messages - uz, uz-Cyrl, ru, en
);
$response = $notify->sms()->send('+998901234567', 'Salom!');RSA signing, OAuth tokens, retries, Idempotency-Key, and every channel work exactly as in the Laravel SDK, since cloudme/notify-laravel is built directly on top of this package.
OTP (one-time codes)
The platform generates, delivers and verifies the code - your app never sees or stores it. Code length, validity and allowed attempts come from your company's OTP settings in the Notify cabinet, or the defaults set by the administrator when you have not set your own. OTP messages are billed like normal messages and are never held for moderation.
// 1. Send a code - keep the otp_id
$otp = $notify->otp()->send(to: '+998901234567');
// 2. Verify what the user typed
$result = $notify->otp()->verify($otp->otpId, $codeFromUser);
$result->verified; // true | false
$result->errorCode; // OTP_INVALID | OTP_EXPIRED | OTP_ATTEMPTS_EXCEEDED | OTP_ALREADY_VERIFIED
$result->attemptsLeft;
$result->canRetry();Free text that fits no template is held for moderation ($response->isHeldForModeration()). Use a template or the OTP API for OTPs and time-sensitive messages.
Photos and formatting
Attach a public JPEG/PNG URL to Telegram and WhatsApp messages with photoUrl - the text becomes the photo's caption (text over 1024 characters follows as a separate message). Telegram renders HTML (<b>, <i>, <u>, <a href>); WhatsApp uses *bold*, _italic_, ~strike~.
$notify->telegram()->send(
to: '+998901234567',
message: "<b>Buyurtmangiz tayyor</b>\nModel: DAF XF106",
photoUrl: 'https://example.com/photos/order.jpg',
);
$notify->whatsapp()->send(to: '+998901234567', message: '*Buyurtmangiz tayyor*', photoUrl: 'https://example.com/photos/order.jpg');With a WhatsApp template (templateId), photoUrl fills the template's IMAGE header - the template must be approved by Meta with one.
Debt collection
Hand a debt over and Notify reminds the debtor channel by channel following a collection strategy (e.g. Telegram on day 1 after the due date, SMS on day 2, then WhatsApp and a voice call) until you report it paid. Requires the debts:manage scope.
$debt = $notify->debts()->create(
name: 'Aziz Karimov',
phone: '+998901234567',
amount: 1250000,
dueDate: '2026-10-15',
externalId: 'INV-1001',
);
$debt->id; // "0f6c1a52-..."
$debt->nextReminderAt; // "2026-10-16T10:00:00+05:00"
$result = $notify->debts()->recordPayment($debt->id, 1250000, externalId: 'PAY-5001');
$result->debt->isPaid(); // true
$result->payment->attributedChannel; // "sms"
$result->payment->daysLate; // 2
$notify->debts()->find($debt->id)->payments;
$notify->debts()->cancel($debt->id);externalId- your own invoice/contract reference: resending it never creates a duplicate, so calls are only retried automatically when it is set.- Partial payments are accepted - reminders continue for the remainder and stop once the debt is fully paid.
attributedChannel- the last delivered reminder before the payment; the dashboard uses it to show which channel works.- Strategy ID: dashboard -> Collection -> Strategies -> API ID. Omit it to use your default strategy.
- Errors:
EXCEEDS_REMAINING,DEBT_CLOSED,INVALID_AMOUNT,STRATEGY_NOT_FOUND(ValidationException).
Scheduled messages
Send a message later - once or on a recurring timetable (every N hours/days, on chosen weekdays or days of the month) - then edit, pause, resume or cancel it. These are the same schedules as the dashboard's Scheduled messages page. Requires the schedules:manage scope plus the channel's send scope (e.g. sms:send).
use CloudMe\Notify\ScheduledMessages\Recurrence;
$schedule = $notify->scheduledMessages()->create(
channel: 'sms',
startsAt: '2026-10-06T10:00:00+05:00',
recipient: '+998901234567',
message: 'Eslatma: bugun soat 15:00 da uchrashuv',
);
$schedule->id; // "7b1e0c4a-..."
$schedule->nextRunAt; // "2026-10-06T10:00:00+05:00"
$notify->scheduledMessages()->create(
channel: 'telegram',
startsAt: '2026-10-05T09:00:00+05:00',
contactGroupId: 12,
message: 'Salom, {{name}}! Haftalik yangiliklar...',
recurrence: Recurrence::weekly([1, 4]), // Recurrence::every(2, 'hours'), Recurrence::monthly([1, 15])
);
$notify->scheduledMessages()->update($schedule->id, channel: 'sms', startsAt: '2026-10-07T10:00:00+05:00', recipient: '+998901234567', message: 'Uchrashuv ertaga');
$notify->scheduledMessages()->pause($schedule->id);
$notify->scheduledMessages()->resume($schedule->id);
$notify->scheduledMessages()->cancel($schedule->id);
$notify->scheduledMessages()->list(status: 'active')->items;- Without
recurrencethe message is sent once atstartsAt. Weekly/monthly sends happen at the time of day ofstartsAt. - Groups: pass
contactGroupId(dashboard -> Contacts -> Groups -> API ID) instead ofrecipient;{{name}}in the text becomes each contact's name. Production only. update()replaces the whole schedule - pass every field again.resume()continues at the next occurrence; sends missed while paused are skipped.- Each send is a normal, billed message (free in sandbox). A failed send (e.g. insufficient balance) is recorded in
lastErrorand the schedule moves on to its next occurrence. - Errors:
SCHEDULE_STATE_INVALID,SCHEDULE_START_PASSED,TEMPLATE_NOT_FOUND,CONTACT_GROUP_NOT_FOUND,SANDBOX_GROUP_UNSUPPORTED(ValidationException).
Receiving webhooks
Webhooks are registered per API client (dashboard -> API clients -> Webhook). Every request is signed: X-Signature is the HMAC-SHA256 of "{X-Webhook-Timestamp}.{raw body}". Requests older than 5 minutes are rejected, and X-Webhook-Id stays the same across retries.
Always pass the raw request body - a re-encoded array breaks the signature:
use CloudMe\Notify\Exceptions\InvalidWebhookSignatureException;
use CloudMe\Notify\Webhooks\WebhookVerifier;
$verifier = new WebhookVerifier(getenv('NOTIFY_WEBHOOK_SECRET'));
try {
$event = $verifier->parse(file_get_contents('php://input'), getallheaders());
} catch (InvalidWebhookSignatureException) {
http_response_code(401);
exit;
}
// $event->id, $event->event, $event->messageId, $event->status, $event->errorCode
http_response_code(200);Links
cloudme-notify
Rasmiy Python SDK - RSA imzolash, OAuth token, retry va Idempotency-Key avtomatik boshqariladi. Django loyihalar uchun uning ustiga qurilgan cloudme-notify-django tavsiya etiladi.
pip install cloudme-notifyO‘rnatish
Python 3.9+ kerak. Paketni pip orqali o‘rnating:
pip install cloudme-notifyNotify panelidagi API client'ning client_id, api_key va RSA private key'i kerak bo'ladi.
Tezkor boshlash
import os
from cloudme_notify import NotifyClient
notify = NotifyClient(
client_id=os.environ["NOTIFY_CLIENT_ID"],
api_key=os.environ["NOTIFY_API_KEY"],
private_key="/secure/path/private.pem",
base_url=NotifyClient.SANDBOX_URL, # NotifyClient.PRODUCTION_URL
locale="uz", # uz, uz-Cyrl, ru, en
)
response = notify.sms.send(to="+998901234567", message="Salom!")
response.message_id # "0f6c1a52-..."
response.environment # "sandbox"Birinchi chaqiruvda SDK kalitlaringizni imzolab access token oladi, uni keshlaydi va keyin avtomatik yangilaydi.
Sandbox
Sandbox client (nk_test_...) faqat NotifyClient.SANDBOX_URL bilan, production client faqat NotifyClient.PRODUCTION_URL bilan ishlaydi (aks holda ENVIRONMENT_MISMATCH). Sandbox xabarlari haqiqiy provayderga bormaydi va balansdan pul yechilmaydi, lekin haqiqiy message_id, status va price qaytadi.
Xabar yuborish
Har bir kanalda bir xil send():
notify.sms.send(to="+998901234567", message="Salom!", sms_type="service")
notify.telegram.send(to="+998901234567", message="<b>Tayyor</b>", photo_url="https://example.com/a.jpg")
notify.whatsapp.send(to="+998901234567", message="*Tayyor*")
notify.email.send(to="user@example.com", subject="Hisob-faktura", message="...")
notify.push.send(to="+998901234567", subject="Yangi xabar", message="...")
notify.voice.call(to="+998901234567", message="Buyurtmangiz tayyor")
notify.sms.send(to="+998901234567", template_id=42, variables={"name": "Aziz"})
notify.sms.send(to="+998901234567", message="...", idempotency_key="order-1001")
notify.message(response.message_id).status # "delivered"
notify.balance().balance # "1000000.00"- Hech bir shablonga mos kelmagan erkin matn moderatsiyaga tushadi:
response.is_held_for_moderation(). - Har bir yuborishda
Idempotency-Keybo'ladi (berilmasa generatsiya qilinadi) - tarmoq xatosidan keyingi avtomatik retry xabarni ikki marta yubormaydi. channel_account_id- o'zingizning yuboruvchi profilingiz (panel -> Kanallar).
Shablonlar, Telegram va Push
for template in notify.templates.list(channel="sms"):
print(template.id, template.name, template.variables)
binding = notify.telegram.binding_link("+998901234567")
binding.bound # True once linked
binding.link # "https://t.me/notify_bot?start=..."
notify.push.register_device(phone="+998901234567", fcm_token=token_from_app)Telegram telefon raqamiga to'g'ridan-to'g'ri yozolmaydi: mijozga bog'lash havolasini yuboring, u Start bosgach xabarlar yetkaziladi.
OTP (bir martalik kod)
Kodni platforma yaratadi, yuboradi va tekshiradi - ilovangiz kodni ko'rmaydi.
otp = notify.otp.send(to="+998901234567") # channel="sms" | "telegram" | "whatsapp" | "email"
result = notify.otp.verify(otp.otp_id, code_from_user)
result.verified # True / False
result.error_code # OTP_INVALID | OTP_EXPIRED | OTP_ATTEMPTS_EXCEEDED | OTP_ALREADY_VERIFIED
result.can_retry()Noto'g'ri kod - istisno emas, natija. Juda tez qayta so'rash RateLimitError (retry_after_seconds) beradi. Sandbox'da otp.code test uchun qaytadi.
Rejali xabarlar
Xabarni bir marta yoki takroriy jadval bo'yicha keyinroq yuboring, keyin o'zgartiring, to'xtating, davom ettiring yoki bekor qiling. schedules:manage va kanalning yuborish scope'i kerak.
from datetime import datetime
from zoneinfo import ZoneInfo
from cloudme_notify import Recurrence
schedule = notify.scheduled_messages.create(
channel="sms",
starts_at=datetime(2026, 10, 6, 10, 0, tzinfo=ZoneInfo("Asia/Tashkent")),
recipient="+998901234567",
message="Eslatma: bugun soat 15:00 da uchrashuv",
)
notify.scheduled_messages.create(
channel="telegram",
starts_at="2026-10-05T09:00:00+05:00",
contact_group_id=12,
message="Salom, {{name}}!",
recurrence=Recurrence.weekly([1, 4]), # Recurrence.every(2, "hours"), Recurrence.monthly([1, 15])
)
notify.scheduled_messages.pause(schedule.id)
notify.scheduled_messages.resume(schedule.id)
notify.scheduled_messages.cancel(schedule.id)
notify.scheduled_messages.list(status="active").itemsGuruhga yuborish (contact_group_id) faqat production'da. Xatolar: SCHEDULE_STATE_INVALID, SCHEDULE_START_PASSED, TEMPLATE_NOT_FOUND, CONTACT_GROUP_NOT_FOUND, SANDBOX_GROUP_UNSUPPORTED.
Qarz undirish
Qarzni topshiring - Notify to'lov xabar qilinmaguncha qarzdorga kanalma-kanal eslatma yuboradi. debts:manage scope kerak.
from datetime import date
debt = notify.debts.create(
name="Aziz Karimov",
phone="+998901234567",
amount=1250000,
due_date=date(2026, 10, 15),
external_id="INV-1001",
)
result = notify.debts.record_payment(debt.id, 1250000, external_id="PAY-5001")
result.debt.is_paid() # True
result.payment.attributed_channel # "sms"
notify.debts.cancel(debt.id)Webhook'larni qabul qilish
Har doim so'rovning xom tanasini bering. Qayta yuborilgan event bir xil event.id bilan keladi - takrorlarni shu bo'yicha o'tkazib yuboring.
from cloudme_notify import InvalidWebhookSignatureError, WebhookVerifier
verifier = WebhookVerifier(os.environ["NOTIFY_WEBHOOK_SECRET"])
try:
event = verifier.parse(raw_body_bytes, request_headers)
except InvalidWebhookSignatureError:
... # 401
if event.event == "message.failed":
mark_failed(event.message_id, event.error_code)Xatoliklar bilan ishlash
from cloudme_notify import InsufficientBalanceError, NotifyError, ValidationError
try:
notify.sms.send(to="+998901234567", message="...")
except InsufficientBalanceError:
...
except ValidationError as error:
error.error_code, error.errors, error.hint
except NotifyError as error:
error.error_code, error.status_code, error.docs_urlBarcha API xatolari NotifyErrordan meros oladi: error_code, status_code, hint (tanlangan tilda), docs_url. Lokal sozlash xatosi - ConfigurationError.
Havolalar
cloudme-notify
Расмий Python SDK - RSA имзолаш, OAuth token, retry ва Idempotency-Key автоматик бошқарилади. Django лойиҳалар учун унинг устига қурилган cloudme-notify-django тавсия этилади.
pip install cloudme-notifyЎрнатиш
Python 3.9+ керак. Пакетни pip орқали ўрнатинг:
pip install cloudme-notifyNotify панелидаги API client'нинг client_id, api_key ва RSA private key'и керак бўлади.
Тезкор бошлаш
import os
from cloudme_notify import NotifyClient
notify = NotifyClient(
client_id=os.environ["NOTIFY_CLIENT_ID"],
api_key=os.environ["NOTIFY_API_KEY"],
private_key="/secure/path/private.pem",
base_url=NotifyClient.SANDBOX_URL, # NotifyClient.PRODUCTION_URL
locale="uz", # uz, uz-Cyrl, ru, en
)
response = notify.sms.send(to="+998901234567", message="Salom!")
response.message_id # "0f6c1a52-..."
response.environment # "sandbox"Биринчи чақирувда SDK калитларингизни имзолаб access token олади, уни кешлайди ва кейин автоматик янгилайди.
Sandbox
Sandbox client (nk_test_...) фақат NotifyClient.SANDBOX_URL билан, production client фақат NotifyClient.PRODUCTION_URL билан ишлайди (акс ҳолда ENVIRONMENT_MISMATCH). Sandbox хабарлари ҳақиқий провайдерга бормайди ва балансдан пул ечилмайди, лекин ҳақиқий message_id, status ва price қайтади.
Хабар юбориш
Ҳар бир каналда бир хил send():
notify.sms.send(to="+998901234567", message="Salom!", sms_type="service")
notify.telegram.send(to="+998901234567", message="<b>Tayyor</b>", photo_url="https://example.com/a.jpg")
notify.whatsapp.send(to="+998901234567", message="*Tayyor*")
notify.email.send(to="user@example.com", subject="Hisob-faktura", message="...")
notify.push.send(to="+998901234567", subject="Yangi xabar", message="...")
notify.voice.call(to="+998901234567", message="Buyurtmangiz tayyor")
notify.sms.send(to="+998901234567", template_id=42, variables={"name": "Aziz"})
notify.sms.send(to="+998901234567", message="...", idempotency_key="order-1001")
notify.message(response.message_id).status # "delivered"
notify.balance().balance # "1000000.00"- Ҳеч бир шаблонга мос келмаган эркин матн модерацияга тушади:
response.is_held_for_moderation(). - Ҳар бир юборишда
Idempotency-Keyбўлади (берилмаса генерация қилинади) - тармоқ хатосидан кейинги автоматик retry хабарни икки марта юбормайди. channel_account_id- ўзингизнинг юборувчи профилингиз (панел -> Каналлар).
Шаблонлар, Telegram ва Push
for template in notify.templates.list(channel="sms"):
print(template.id, template.name, template.variables)
binding = notify.telegram.binding_link("+998901234567")
binding.bound # True once linked
binding.link # "https://t.me/notify_bot?start=..."
notify.push.register_device(phone="+998901234567", fcm_token=token_from_app)Telegram телефон рақамига тўғридан-тўғри ёзолмайди: мижозга боғлаш ҳаволасини юборинг, у Start босгач хабарлар етказилади.
OTP (бир мартали код)
Кодни платформа яратади, юборади ва текширади - иловангиз кодни кўрмайди.
otp = notify.otp.send(to="+998901234567") # channel="sms" | "telegram" | "whatsapp" | "email"
result = notify.otp.verify(otp.otp_id, code_from_user)
result.verified # True / False
result.error_code # OTP_INVALID | OTP_EXPIRED | OTP_ATTEMPTS_EXCEEDED | OTP_ALREADY_VERIFIED
result.can_retry()Нотўғри код - истисно эмас, натижа. Жуда тез қайта сўраш RateLimitError (retry_after_seconds) беради. Sandbox'да otp.code тест учун қайтади.
Режали хабарлар
Хабарни бир марта ёки такрорий жадвал бўйича кейинроқ юборинг, кейин ўзгартиринг, тўхтатинг, давом эттиринг ёки бекор қилинг. schedules:manage ва каналнинг юбориш scope'и керак.
from datetime import datetime
from zoneinfo import ZoneInfo
from cloudme_notify import Recurrence
schedule = notify.scheduled_messages.create(
channel="sms",
starts_at=datetime(2026, 10, 6, 10, 0, tzinfo=ZoneInfo("Asia/Tashkent")),
recipient="+998901234567",
message="Eslatma: bugun soat 15:00 da uchrashuv",
)
notify.scheduled_messages.create(
channel="telegram",
starts_at="2026-10-05T09:00:00+05:00",
contact_group_id=12,
message="Salom, {{name}}!",
recurrence=Recurrence.weekly([1, 4]), # Recurrence.every(2, "hours"), Recurrence.monthly([1, 15])
)
notify.scheduled_messages.pause(schedule.id)
notify.scheduled_messages.resume(schedule.id)
notify.scheduled_messages.cancel(schedule.id)
notify.scheduled_messages.list(status="active").itemsГуруҳга юбориш (contact_group_id) фақат production'да. Хатолар: SCHEDULE_STATE_INVALID, SCHEDULE_START_PASSED, TEMPLATE_NOT_FOUND, CONTACT_GROUP_NOT_FOUND, SANDBOX_GROUP_UNSUPPORTED.
Қарз ундириш
Қарзни топширинг - Notify тўлов хабар қилинмагунча қарздорга каналма-канал эслатма юборади. debts:manage scope керак.
from datetime import date
debt = notify.debts.create(
name="Aziz Karimov",
phone="+998901234567",
amount=1250000,
due_date=date(2026, 10, 15),
external_id="INV-1001",
)
result = notify.debts.record_payment(debt.id, 1250000, external_id="PAY-5001")
result.debt.is_paid() # True
result.payment.attributed_channel # "sms"
notify.debts.cancel(debt.id)Webhook'ларни қабул қилиш
Ҳар доим сўровнинг хом танасини беринг. Қайта юборилган event бир хил event.id билан келади - такрорларни шу бўйича ўтказиб юборинг.
from cloudme_notify import InvalidWebhookSignatureError, WebhookVerifier
verifier = WebhookVerifier(os.environ["NOTIFY_WEBHOOK_SECRET"])
try:
event = verifier.parse(raw_body_bytes, request_headers)
except InvalidWebhookSignatureError:
... # 401
if event.event == "message.failed":
mark_failed(event.message_id, event.error_code)Хатоликлар билан ишлаш
from cloudme_notify import InsufficientBalanceError, NotifyError, ValidationError
try:
notify.sms.send(to="+998901234567", message="...")
except InsufficientBalanceError:
...
except ValidationError as error:
error.error_code, error.errors, error.hint
except NotifyError as error:
error.error_code, error.status_code, error.docs_urlБарча API хатолари NotifyErrorдан мерос олади: error_code, status_code, hint (танланган тилда), docs_url. Локал созлаш хатоси - ConfigurationError.
Ҳаволалар
cloudme-notify
Официальный Python SDK - RSA-подпись, OAuth-токены, повторы и Idempotency-Key обрабатываются автоматически. Для Django-проектов рекомендуется построенный поверх него cloudme-notify-django.
pip install cloudme-notifyУстановка
Нужен Python 3.9+. Установите пакет через pip:
pip install cloudme-notifyПонадобятся client_id, api_key и RSA private key API-клиента из панели Notify.
Быстрый старт
import os
from cloudme_notify import NotifyClient
notify = NotifyClient(
client_id=os.environ["NOTIFY_CLIENT_ID"],
api_key=os.environ["NOTIFY_API_KEY"],
private_key="/secure/path/private.pem",
base_url=NotifyClient.SANDBOX_URL, # NotifyClient.PRODUCTION_URL
locale="uz", # uz, uz-Cyrl, ru, en
)
response = notify.sms.send(to="+998901234567", message="Salom!")
response.message_id # "0f6c1a52-..."
response.environment # "sandbox"При первом вызове SDK подписывает ваши ключи и получает access token, кеширует его и дальше обновляет автоматически.
Sandbox
Sandbox-клиент (nk_test_...) работает только с NotifyClient.SANDBOX_URL, production-клиент - только с NotifyClient.PRODUCTION_URL (иначе ENVIRONMENT_MISMATCH). Сообщения sandbox не уходят реальному провайдеру и не списывают баланс, но возвращают настоящие message_id, status и price.
Отправка сообщений
У каждого канала одинаковый send():
notify.sms.send(to="+998901234567", message="Salom!", sms_type="service")
notify.telegram.send(to="+998901234567", message="<b>Tayyor</b>", photo_url="https://example.com/a.jpg")
notify.whatsapp.send(to="+998901234567", message="*Tayyor*")
notify.email.send(to="user@example.com", subject="Hisob-faktura", message="...")
notify.push.send(to="+998901234567", subject="Yangi xabar", message="...")
notify.voice.call(to="+998901234567", message="Buyurtmangiz tayyor")
notify.sms.send(to="+998901234567", template_id=42, variables={"name": "Aziz"})
notify.sms.send(to="+998901234567", message="...", idempotency_key="order-1001")
notify.message(response.message_id).status # "delivered"
notify.balance().balance # "1000000.00"- Свободный текст, не подходящий ни под один шаблон, уходит на модерацию:
response.is_held_for_moderation(). - Каждая отправка несёт
Idempotency-Key(генерируется, если не передан) - автоматический повтор после сетевой ошибки не отправит сообщение дважды. channel_account_id- ваш профиль отправителя (панель -> Каналы).
Шаблоны, Telegram и Push
for template in notify.templates.list(channel="sms"):
print(template.id, template.name, template.variables)
binding = notify.telegram.binding_link("+998901234567")
binding.bound # True once linked
binding.link # "https://t.me/notify_bot?start=..."
notify.push.register_device(phone="+998901234567", fcm_token=token_from_app)Telegram не может написать на номер телефона напрямую: отправьте клиенту ссылку привязки - после нажатия Start сообщения будут доставляться.
OTP (одноразовый код)
Платформа сама генерирует, отправляет и проверяет код - ваше приложение его не видит.
otp = notify.otp.send(to="+998901234567") # channel="sms" | "telegram" | "whatsapp" | "email"
result = notify.otp.verify(otp.otp_id, code_from_user)
result.verified # True / False
result.error_code # OTP_INVALID | OTP_EXPIRED | OTP_ATTEMPTS_EXCEEDED | OTP_ALREADY_VERIFIED
result.can_retry()Неверный код - это результат, а не исключение. Слишком частый запрос даёт RateLimitError (retry_after_seconds). В sandbox otp.code возвращается для тестов.
Запланированные сообщения
Отправьте сообщение позже - разово или по расписанию, затем изменяйте, приостанавливайте, возобновляйте или отменяйте. Нужен scope schedules:manage и scope отправки канала.
from datetime import datetime
from zoneinfo import ZoneInfo
from cloudme_notify import Recurrence
schedule = notify.scheduled_messages.create(
channel="sms",
starts_at=datetime(2026, 10, 6, 10, 0, tzinfo=ZoneInfo("Asia/Tashkent")),
recipient="+998901234567",
message="Eslatma: bugun soat 15:00 da uchrashuv",
)
notify.scheduled_messages.create(
channel="telegram",
starts_at="2026-10-05T09:00:00+05:00",
contact_group_id=12,
message="Salom, {{name}}!",
recurrence=Recurrence.weekly([1, 4]), # Recurrence.every(2, "hours"), Recurrence.monthly([1, 15])
)
notify.scheduled_messages.pause(schedule.id)
notify.scheduled_messages.resume(schedule.id)
notify.scheduled_messages.cancel(schedule.id)
notify.scheduled_messages.list(status="active").itemsОтправка группе (contact_group_id) - только в production. Ошибки: SCHEDULE_STATE_INVALID, SCHEDULE_START_PASSED, TEMPLATE_NOT_FOUND, CONTACT_GROUP_NOT_FOUND, SANDBOX_GROUP_UNSUPPORTED.
Взыскание долгов
Передайте долг - Notify будет напоминать должнику по каналам, пока вы не сообщите об оплате. Нужен scope debts:manage.
from datetime import date
debt = notify.debts.create(
name="Aziz Karimov",
phone="+998901234567",
amount=1250000,
due_date=date(2026, 10, 15),
external_id="INV-1001",
)
result = notify.debts.record_payment(debt.id, 1250000, external_id="PAY-5001")
result.debt.is_paid() # True
result.payment.attributed_channel # "sms"
notify.debts.cancel(debt.id)Приём вебхуков
Всегда передавайте сырое тело запроса. Повторная доставка приходит с тем же event.id - пропускайте дубли по нему.
from cloudme_notify import InvalidWebhookSignatureError, WebhookVerifier
verifier = WebhookVerifier(os.environ["NOTIFY_WEBHOOK_SECRET"])
try:
event = verifier.parse(raw_body_bytes, request_headers)
except InvalidWebhookSignatureError:
... # 401
if event.event == "message.failed":
mark_failed(event.message_id, event.error_code)Обработка ошибок
from cloudme_notify import InsufficientBalanceError, NotifyError, ValidationError
try:
notify.sms.send(to="+998901234567", message="...")
except InsufficientBalanceError:
...
except ValidationError as error:
error.error_code, error.errors, error.hint
except NotifyError as error:
error.error_code, error.status_code, error.docs_urlВсе ошибки API наследуют NotifyError: error_code, status_code, hint (на выбранном языке), docs_url. Ошибка локальной настройки - ConfigurationError.
Ссылки
cloudme-notify
The official Python SDK - RSA signing, OAuth tokens, retries and Idempotency-Key are handled for you. For Django projects, cloudme-notify-django (built on top of it) is recommended.
pip install cloudme-notifyInstallation
Requires Python 3.9+. Install with pip:
pip install cloudme-notifyYou need the API client's client_id, api_key and RSA private key from the Notify dashboard.
Quick start
import os
from cloudme_notify import NotifyClient
notify = NotifyClient(
client_id=os.environ["NOTIFY_CLIENT_ID"],
api_key=os.environ["NOTIFY_API_KEY"],
private_key="/secure/path/private.pem",
base_url=NotifyClient.SANDBOX_URL, # NotifyClient.PRODUCTION_URL
locale="uz", # uz, uz-Cyrl, ru, en
)
response = notify.sms.send(to="+998901234567", message="Salom!")
response.message_id # "0f6c1a52-..."
response.environment # "sandbox"On the first call the SDK signs your credentials, gets an access token, caches it and refreshes it automatically from then on.
Sandbox
A sandbox client (nk_test_...) works only with NotifyClient.SANDBOX_URL, a production one only with NotifyClient.PRODUCTION_URL (ENVIRONMENT_MISMATCH otherwise). Sandbox sends never reach a real provider and never charge your balance, but return a real message_id, status and price.
Sending messages
Every channel has the same send():
notify.sms.send(to="+998901234567", message="Salom!", sms_type="service")
notify.telegram.send(to="+998901234567", message="<b>Tayyor</b>", photo_url="https://example.com/a.jpg")
notify.whatsapp.send(to="+998901234567", message="*Tayyor*")
notify.email.send(to="user@example.com", subject="Hisob-faktura", message="...")
notify.push.send(to="+998901234567", subject="Yangi xabar", message="...")
notify.voice.call(to="+998901234567", message="Buyurtmangiz tayyor")
notify.sms.send(to="+998901234567", template_id=42, variables={"name": "Aziz"})
notify.sms.send(to="+998901234567", message="...", idempotency_key="order-1001")
notify.message(response.message_id).status # "delivered"
notify.balance().balance # "1000000.00"- Free text matching none of your templates is held for moderation:
response.is_held_for_moderation(). - Every send carries an
Idempotency-Key(generated if not given), so the automatic retry after a network failure never sends twice. channel_account_idpicks one of your own sender profiles (dashboard -> Channels).
Templates, Telegram and Push
for template in notify.templates.list(channel="sms"):
print(template.id, template.name, template.variables)
binding = notify.telegram.binding_link("+998901234567")
binding.bound # True once linked
binding.link # "https://t.me/notify_bot?start=..."
notify.push.register_device(phone="+998901234567", fcm_token=token_from_app)Telegram cannot message a phone number directly: send the customer the binding link; once they press Start, messages are delivered.
One-time codes (OTP)
The platform generates, delivers and checks the code - your app never sees it.
otp = notify.otp.send(to="+998901234567") # channel="sms" | "telegram" | "whatsapp" | "email"
result = notify.otp.verify(otp.otp_id, code_from_user)
result.verified # True / False
result.error_code # OTP_INVALID | OTP_EXPIRED | OTP_ATTEMPTS_EXCEEDED | OTP_ALREADY_VERIFIED
result.can_retry()A wrong code is a result, not an exception. Requesting again too soon raises RateLimitError (retry_after_seconds). In sandbox otp.code is returned for testing.
Scheduled messages
Send a message later - once or on a recurring timetable - then edit, pause, resume or cancel it. Requires schedules:manage plus the channel's send scope.
from datetime import datetime
from zoneinfo import ZoneInfo
from cloudme_notify import Recurrence
schedule = notify.scheduled_messages.create(
channel="sms",
starts_at=datetime(2026, 10, 6, 10, 0, tzinfo=ZoneInfo("Asia/Tashkent")),
recipient="+998901234567",
message="Eslatma: bugun soat 15:00 da uchrashuv",
)
notify.scheduled_messages.create(
channel="telegram",
starts_at="2026-10-05T09:00:00+05:00",
contact_group_id=12,
message="Salom, {{name}}!",
recurrence=Recurrence.weekly([1, 4]), # Recurrence.every(2, "hours"), Recurrence.monthly([1, 15])
)
notify.scheduled_messages.pause(schedule.id)
notify.scheduled_messages.resume(schedule.id)
notify.scheduled_messages.cancel(schedule.id)
notify.scheduled_messages.list(status="active").itemsGroup targets (contact_group_id) are production only. Errors: SCHEDULE_STATE_INVALID, SCHEDULE_START_PASSED, TEMPLATE_NOT_FOUND, CONTACT_GROUP_NOT_FOUND, SANDBOX_GROUP_UNSUPPORTED.
Debt collection
Hand a debt over and Notify reminds the debtor channel by channel until you report it paid. Requires the debts:manage scope.
from datetime import date
debt = notify.debts.create(
name="Aziz Karimov",
phone="+998901234567",
amount=1250000,
due_date=date(2026, 10, 15),
external_id="INV-1001",
)
result = notify.debts.record_payment(debt.id, 1250000, external_id="PAY-5001")
result.debt.is_paid() # True
result.payment.attributed_channel # "sms"
notify.debts.cancel(debt.id)Receiving webhooks
Always pass the raw request body. A retried delivery carries the same event.id - dedupe on it.
from cloudme_notify import InvalidWebhookSignatureError, WebhookVerifier
verifier = WebhookVerifier(os.environ["NOTIFY_WEBHOOK_SECRET"])
try:
event = verifier.parse(raw_body_bytes, request_headers)
except InvalidWebhookSignatureError:
... # 401
if event.event == "message.failed":
mark_failed(event.message_id, event.error_code)Error handling
from cloudme_notify import InsufficientBalanceError, NotifyError, ValidationError
try:
notify.sms.send(to="+998901234567", message="...")
except InsufficientBalanceError:
...
except ValidationError as error:
error.error_code, error.errors, error.hint
except NotifyError as error:
error.error_code, error.status_code, error.docs_urlEvery API error derives from NotifyError: error_code, status_code, hint (in your locale), docs_url. A local setup problem raises ConfigurationError.
Links
cloudme-notify-django
Django uchun rasmiy integratsiya: settings orqali sozlash, tayyor notify klienti, tokenni Django cache'da saqlash, webhook view va notify_doctor buyrug'i.
pip install cloudme-notify-djangoO‘rnatish va sozlash
pip install cloudme-notify-django# settings.py
INSTALLED_APPS = [
# ...
"cloudme_notify_django",
]
NOTIFY = {
"ENVIRONMENT": "sandbox", # "production"
"CLIENT_ID": os.environ["NOTIFY_CLIENT_ID"],
"API_KEY": os.environ["NOTIFY_API_KEY"],
"PRIVATE_KEY_PATH": BASE_DIR / "secrets" / "notify.pem",
"WEBHOOK_SECRET": os.environ.get("NOTIFY_WEBHOOK_SECRET"),
"LOCALE": "uz",
}NOTIFYdagi har bir kalit o'rniga muhit o'zgaruvchisi ishlatilishi mumkin (NOTIFY_CLIENT_ID, NOTIFY_API_KEY, NOTIFY_PRIVATE_KEY_PATH, NOTIFY_WEBHOOK_SECRET...). BASE_URL faqat maxsus deploy uchun.
Diagnostika
python manage.py notify_doctorHech qanday xabar yubormaydi va maxfiy qiymatlarni chiqarmaydi - production'da ham xavfsiz.
Yuborish
from cloudme_notify_django import notify
notify.sms.send(to="+998901234567", message="Buyurtmangiz tayyor")
notify.telegram.send(to=user.phone, template_id=42, variables={"name": user.first_name})
otp = notify.otp.send(to=request.POST["phone"])
request.session["otp_id"] = otp.otp_id
notify.scheduled_messages.create(
channel="sms",
starts_at=appointment.starts_at - timedelta(days=1),
recipient=customer.phone,
message="Eslatma: ertaga qabul",
)notify - to'liq cloudme-notify klienti (otp, templates, debts, scheduled_messages, balance()...). Access token Django cache'da saqlanadi, shuning uchun web worker'lar va Celery task'lar uni qayta ishlatadi. Ommaviy yoki shoshilinch bo'lmagan yuborishlarni Celery task'dan chaqiring.
Webhook'larni qabul qilish
# urls.py
from cloudme_notify_django.webhooks import webhook_view
urlpatterns = [path("webhooks/notify/", webhook_view)]
# yourapp/signals.py
from django.dispatch import receiver
from cloudme_notify_django.signals import webhook_received
@receiver(webhook_received)
def on_notify_event(sender, event, **kwargs):
if event.event == "message.failed":
Order.objects.filter(notify_message_id=event.message_id).update(sms_failed=True)View CSRF'dan ozod, faqat POST qabul qiladi, imzo va vaqtni tekshiradi (aks holda 401) va 200 qaytaradi. Notify 5 soniya kutadi - receiver'larni tez qiling. O'z view'ingiz uchun dekorator:
from cloudme_notify_django.webhooks import notify_webhook
@notify_webhook
def notify_events(request):
event = request.notify_event
return HttpResponse(status=204)Testlash
Testlarda klientni httpx.MockTransportdagi klientga almashtiring - API'ga so'rov ketmaydi:
import httpx
from cloudme_notify import NotifyClient
from cloudme_notify_django import set_client
def handler(request):
if request.url.path.endswith("/oauth/token"):
return httpx.Response(200, json={"access_token": "t", "expires_in": 900, "refresh_token": "r"})
return httpx.Response(200, json={"success": True, "message_id": "m-1", "status": "queued", "price": "0", "currency": "UZS"})
set_client(NotifyClient(client_id="c", api_key="k", private_key=TEST_PEM,
base_url=NotifyClient.SANDBOX_URL, transport=httpx.MockTransport(handler)))settings.NOTIFY o'zgarsa (override_settings), klient avtomatik qayta yaratiladi.
Havolalar
cloudme-notify-django
Django учун расмий интеграция: settings орқали созлаш, тайёр notify клиенти, токенни Django cache'да сақлаш, webhook view ва notify_doctor буйруғи.
pip install cloudme-notify-djangoЎрнатиш ва созлаш
pip install cloudme-notify-django# settings.py
INSTALLED_APPS = [
# ...
"cloudme_notify_django",
]
NOTIFY = {
"ENVIRONMENT": "sandbox", # "production"
"CLIENT_ID": os.environ["NOTIFY_CLIENT_ID"],
"API_KEY": os.environ["NOTIFY_API_KEY"],
"PRIVATE_KEY_PATH": BASE_DIR / "secrets" / "notify.pem",
"WEBHOOK_SECRET": os.environ.get("NOTIFY_WEBHOOK_SECRET"),
"LOCALE": "uz",
}NOTIFYдаги ҳар бир калит ўрнига муҳит ўзгарувчиси ишлатилиши мумкин (NOTIFY_CLIENT_ID, NOTIFY_API_KEY, NOTIFY_PRIVATE_KEY_PATH, NOTIFY_WEBHOOK_SECRET...). BASE_URL фақат махсус деплой учун.
Диагностика
python manage.py notify_doctorҲеч қандай хабар юбормайди ва махфий қийматларни чиқармайди - production'да ҳам хавфсиз.
Юбориш
from cloudme_notify_django import notify
notify.sms.send(to="+998901234567", message="Buyurtmangiz tayyor")
notify.telegram.send(to=user.phone, template_id=42, variables={"name": user.first_name})
otp = notify.otp.send(to=request.POST["phone"])
request.session["otp_id"] = otp.otp_id
notify.scheduled_messages.create(
channel="sms",
starts_at=appointment.starts_at - timedelta(days=1),
recipient=customer.phone,
message="Eslatma: ertaga qabul",
)notify - тўлиқ cloudme-notify клиенти (otp, templates, debts, scheduled_messages, balance()...). Access token Django cache'да сақланади, шунинг учун web worker'лар ва Celery task'лар уни қайта ишлатади. Оммавий ёки шошилинч бўлмаган юборишларни Celery task'дан чақиринг.
Webhook'ларни қабул қилиш
# urls.py
from cloudme_notify_django.webhooks import webhook_view
urlpatterns = [path("webhooks/notify/", webhook_view)]
# yourapp/signals.py
from django.dispatch import receiver
from cloudme_notify_django.signals import webhook_received
@receiver(webhook_received)
def on_notify_event(sender, event, **kwargs):
if event.event == "message.failed":
Order.objects.filter(notify_message_id=event.message_id).update(sms_failed=True)View CSRF'дан озод, фақат POST қабул қилади, имзо ва вақтни текширади (акс ҳолда 401) ва 200 қайтаради. Notify 5 сония кутади - receiver'ларни тез қилинг. Ўз view'ингиз учун декоратор:
from cloudme_notify_django.webhooks import notify_webhook
@notify_webhook
def notify_events(request):
event = request.notify_event
return HttpResponse(status=204)Тестлаш
Тестларда клиентни httpx.MockTransportдаги клиентга алмаштиринг - API'га сўров кетмайди:
import httpx
from cloudme_notify import NotifyClient
from cloudme_notify_django import set_client
def handler(request):
if request.url.path.endswith("/oauth/token"):
return httpx.Response(200, json={"access_token": "t", "expires_in": 900, "refresh_token": "r"})
return httpx.Response(200, json={"success": True, "message_id": "m-1", "status": "queued", "price": "0", "currency": "UZS"})
set_client(NotifyClient(client_id="c", api_key="k", private_key=TEST_PEM,
base_url=NotifyClient.SANDBOX_URL, transport=httpx.MockTransport(handler)))settings.NOTIFY ўзгарса (override_settings), клиент автоматик қайта яратилади.
Ҳаволалар
cloudme-notify-django
Официальная интеграция для Django: настройка через settings, готовый клиент notify, хранение токена в кеше Django, webhook-view и команда notify_doctor.
pip install cloudme-notify-djangoУстановка и настройка
pip install cloudme-notify-django# settings.py
INSTALLED_APPS = [
# ...
"cloudme_notify_django",
]
NOTIFY = {
"ENVIRONMENT": "sandbox", # "production"
"CLIENT_ID": os.environ["NOTIFY_CLIENT_ID"],
"API_KEY": os.environ["NOTIFY_API_KEY"],
"PRIVATE_KEY_PATH": BASE_DIR / "secrets" / "notify.pem",
"WEBHOOK_SECRET": os.environ.get("NOTIFY_WEBHOOK_SECRET"),
"LOCALE": "uz",
}Вместо любого ключа в NOTIFY можно задать переменную окружения (NOTIFY_CLIENT_ID, NOTIFY_API_KEY, NOTIFY_PRIVATE_KEY_PATH, NOTIFY_WEBHOOK_SECRET...). BASE_URL - только для собственного развёртывания.
Диагностика
python manage.py notify_doctorНичего не отправляет и не выводит секреты - безопасно и в production.
Отправка
from cloudme_notify_django import notify
notify.sms.send(to="+998901234567", message="Buyurtmangiz tayyor")
notify.telegram.send(to=user.phone, template_id=42, variables={"name": user.first_name})
otp = notify.otp.send(to=request.POST["phone"])
request.session["otp_id"] = otp.otp_id
notify.scheduled_messages.create(
channel="sms",
starts_at=appointment.starts_at - timedelta(days=1),
recipient=customer.phone,
message="Eslatma: ertaga qabul",
)notify - полный клиент cloudme-notify (otp, templates, debts, scheduled_messages, balance()...). Access token хранится в кеше Django, поэтому веб-воркеры и задачи Celery используют его повторно. Массовые и несрочные отправки вызывайте из задач Celery.
Приём вебхуков
# urls.py
from cloudme_notify_django.webhooks import webhook_view
urlpatterns = [path("webhooks/notify/", webhook_view)]
# yourapp/signals.py
from django.dispatch import receiver
from cloudme_notify_django.signals import webhook_received
@receiver(webhook_received)
def on_notify_event(sender, event, **kwargs):
if event.event == "message.failed":
Order.objects.filter(notify_message_id=event.message_id).update(sms_failed=True)View освобождён от CSRF, принимает только POST, проверяет подпись и время (иначе 401) и отвечает 200. Notify ждёт 5 секунд - делайте обработчики быстрыми. Декоратор для своего view:
from cloudme_notify_django.webhooks import notify_webhook
@notify_webhook
def notify_events(request):
event = request.notify_event
return HttpResponse(status=204)Тестирование
В тестах подмените клиент на клиент с httpx.MockTransport - запросы к API не уходят:
import httpx
from cloudme_notify import NotifyClient
from cloudme_notify_django import set_client
def handler(request):
if request.url.path.endswith("/oauth/token"):
return httpx.Response(200, json={"access_token": "t", "expires_in": 900, "refresh_token": "r"})
return httpx.Response(200, json={"success": True, "message_id": "m-1", "status": "queued", "price": "0", "currency": "UZS"})
set_client(NotifyClient(client_id="c", api_key="k", private_key=TEST_PEM,
base_url=NotifyClient.SANDBOX_URL, transport=httpx.MockTransport(handler)))При изменении settings.NOTIFY (override_settings) клиент пересоздаётся автоматически.
Ссылки
cloudme-notify-django
The official Django integration: settings-driven configuration, a ready notify client, token caching in Django's cache, a webhook view and the notify_doctor command.
pip install cloudme-notify-djangoInstallation and settings
pip install cloudme-notify-django# settings.py
INSTALLED_APPS = [
# ...
"cloudme_notify_django",
]
NOTIFY = {
"ENVIRONMENT": "sandbox", # "production"
"CLIENT_ID": os.environ["NOTIFY_CLIENT_ID"],
"API_KEY": os.environ["NOTIFY_API_KEY"],
"PRIVATE_KEY_PATH": BASE_DIR / "secrets" / "notify.pem",
"WEBHOOK_SECRET": os.environ.get("NOTIFY_WEBHOOK_SECRET"),
"LOCALE": "uz",
}Any NOTIFY key can come from an environment variable instead (NOTIFY_CLIENT_ID, NOTIFY_API_KEY, NOTIFY_PRIVATE_KEY_PATH, NOTIFY_WEBHOOK_SECRET...). BASE_URL is only for a custom deployment.
Diagnostics
python manage.py notify_doctorSends nothing and prints no secret - safe in production.
Sending
from cloudme_notify_django import notify
notify.sms.send(to="+998901234567", message="Buyurtmangiz tayyor")
notify.telegram.send(to=user.phone, template_id=42, variables={"name": user.first_name})
otp = notify.otp.send(to=request.POST["phone"])
request.session["otp_id"] = otp.otp_id
notify.scheduled_messages.create(
channel="sms",
starts_at=appointment.starts_at - timedelta(days=1),
recipient=customer.phone,
message="Eslatma: ertaga qabul",
)notify is the full cloudme-notify client (otp, templates, debts, scheduled_messages, balance()...). The access token lives in Django's cache, so web workers and Celery tasks reuse it. Call bulk or non-urgent sends from a Celery task.
Receiving webhooks
# urls.py
from cloudme_notify_django.webhooks import webhook_view
urlpatterns = [path("webhooks/notify/", webhook_view)]
# yourapp/signals.py
from django.dispatch import receiver
from cloudme_notify_django.signals import webhook_received
@receiver(webhook_received)
def on_notify_event(sender, event, **kwargs):
if event.event == "message.failed":
Order.objects.filter(notify_message_id=event.message_id).update(sms_failed=True)The view is CSRF-exempt, accepts only POST, verifies the signature and timestamp (401 otherwise) and answers 200. Notify waits 5 seconds - keep receivers fast. For your own view, use the decorator:
from cloudme_notify_django.webhooks import notify_webhook
@notify_webhook
def notify_events(request):
event = request.notify_event
return HttpResponse(status=204)Testing
In tests, swap the client for one on an httpx.MockTransport - no request reaches the API:
import httpx
from cloudme_notify import NotifyClient
from cloudme_notify_django import set_client
def handler(request):
if request.url.path.endswith("/oauth/token"):
return httpx.Response(200, json={"access_token": "t", "expires_in": 900, "refresh_token": "r"})
return httpx.Response(200, json={"success": True, "message_id": "m-1", "status": "queued", "price": "0", "currency": "UZS"})
set_client(NotifyClient(client_id="c", api_key="k", private_key=TEST_PEM,
base_url=NotifyClient.SANDBOX_URL, transport=httpx.MockTransport(handler)))Changing settings.NOTIFY (override_settings) recreates the client automatically.