Udeyou OTP - Laravel Integration Guide ====================================== Base URL https://udeyou.com/api/v1 Docs https://udeyou.com/docs Format JSON requests and responses (Content-Type: application/json) Auth Authorization: Bearer or X-API-Key: This English guide is the reference version. The code is the same code shown on https://udeyou.com/docs, tested against the live API. 1. Before you start 1) Create an account: https://udeyou.com/register New accounts start a free 2-month trial: 2000 messages in the first month and 2000 more in the second. 2) Create an API key in the dashboard: https://udeyou.com/dashboard#keys The key is shown once. Keep it in the UDEYOU_API_KEY environment variable on your server. 3) Key types udy_test_... No email and no charge, works with a zero balance. The code comes back in the response ("code") together with "test_mode": true. Use it for development and tests. udy_live_... Sends the email and uses 1 credit per code. The credit is refunded automatically if delivery fails. Each key only sees codes sent with its own type. 4) Keep the key on your server. Never put it in browser JavaScript, a mobile app or a code repository. 2. Requirements and setup Laravel 10 or later (PHP 8.1+), using the built-in HTTP client. 1) Add the key to .env (never commit it): UDEYOU_API_KEY=udy_test_... Optional: UDEYOU_BASE_URL (defaults to https://udeyou.com/api/v1) 2) Add the "udeyou" entry to config/services.php. 3) Create app/Services/Udeyou.php and app/Http/Controllers/OtpController.php. 4) Append the routes to routes/web.php and create resources/views/otp.blade.php. 5) Run: php artisan serve, then open /otp in the browser. 3. How the example works - Udeyou::send(), verify() and balance() return Laravel's Illuminate\Http\Client\Response, so you can use ->failed(), ->status() and ->json('data.otp_id') or ->json('error.code'). - The controller keeps otp_id in the server session (never in a hidden field), shows a clear message for each error.code, and stores data.to from the verify response as the verified address. - throttle:5,1 and throttle:10,1 add your own per-user limits on top of the platform limits. - With a test key, Udeyou::send() returns the code in data.code, which is handy in feature tests. 4. Files ----- BEGIN config/services.php ----- // config/services.php: add this entry to the returned array 'udeyou' => [ 'key' => env('UDEYOU_API_KEY'), // .env: UDEYOU_API_KEY=udy_test_... 'url' => env('UDEYOU_BASE_URL', 'https://udeyou.com/api/v1'), ], ----- END config/services.php ----- ----- BEGIN app/Services/Udeyou.php ----- http()->post('/otp/send', ['to' => $to] + $options + ['lang' => 'ar']); } /** POST /otp/verify. data: verified, otp_id, to, verified_at. */ public function verify(string $otpId, string $code): Response { return $this->http()->post('/otp/verify', ['otp_id' => $otpId, 'code' => $code]); } /** GET /balance. data: company, mode, credits. */ public function balance(): Response { return $this->http()->get('/balance'); } private function http(): PendingRequest { return Http::baseUrl(config('services.udeyou.url')) ->withToken(config('services.udeyou.key')) ->acceptJson() ->timeout(20); } } ----- END app/Services/Udeyou.php ----- ----- BEGIN app/Http/Controllers/OtpController.php ----- validate(['email' => ['required', 'email', 'max:190']]); try { $res = $this->udeyou->send($data['email']); } catch (ConnectionException) { return back()->withInput()->withErrors(['email' => 'The service is unavailable. Try again later.']); } if ($res->failed()) { return back()->withInput()->withErrors(['email' => $this->message($res)]); } // Keep otp_id in the server session, never in a hidden form field. $request->session()->put('otp_id', $res->json('data.otp_id')); return back()->with('status', 'We sent a code to your email.'); } // POST /otp/verify (code) public function verify(Request $request): RedirectResponse { $data = $request->validate(['code' => ['required', 'string', 'regex:/^[0-9A-Za-z]{4,10}$/']]); $otpId = $request->session()->get('otp_id'); if (! $otpId) { return back()->withErrors(['code' => 'Request a code first.']); } try { $res = $this->udeyou->verify($otpId, $data['code']); } catch (ConnectionException) { return back()->withErrors(['code' => 'The service is unavailable. Try again later.']); } if ($res->failed()) { return back()->withErrors(['code' => $this->message($res)]); } $request->session()->forget('otp_id'); // data.to is the verified address: sign the user in or mark the email as verified here. $request->session()->put('verified_email', $res->json('data.to')); return redirect()->intended('/'); } private function message(Response $res): string { return match ($res->json('error.code')) { 'invalid_code' => 'Incorrect code. Attempts left: '.$res->json('error.attempts_remaining'), 'resend_too_soon', 'recipient_limit', 'rate_limited' => 'Please wait '.$res->json('error.retry_after').' seconds and try again.', 'otp_expired', 'otp_already_used', 'otp_not_found', 'too_many_attempts' => 'This code is no longer valid. Request a new one.', 'validation_error' => 'Check the email address and try again.', // 401, 402, 403 and 5xx: log $res->json('error') and show a generic message. default => 'The service is unavailable. Try again later.', }; } } ----- END app/Http/Controllers/OtpController.php ----- ----- BEGIN routes/web.php ----- // routes/web.php (Route is already imported at the top of this file) use App\Http\Controllers\OtpController; Route::view('/otp', 'otp'); Route::post('/otp/send', [OtpController::class, 'send'])->middleware('throttle:5,1'); Route::post('/otp/verify', [OtpController::class, 'verify'])->middleware('throttle:10,1'); ----- END routes/web.php ----- ----- BEGIN resources/views/otp.blade.php ----- {{-- resources/views/otp.blade.php --}} @if (session('status'))

{{ session('status') }}

@endif
@csrf @error('email')

{{ $message }}

@enderror
@if (session('otp_id'))
@csrf @error('code')

{{ $message }}

@enderror
@endif ----- END resources/views/otp.blade.php ----- 5. API reference POST /otp/send -> 201 Created to string required Recipient email, up to 190 characters. sender_name string optional Name shown in the inbox, 2-60 characters. Default: your account name. lang string optional "ar" (default) or "en": language of the default message and footer. code_length integer optional 4-10 digits. Default 6. expires_in integer optional Lifetime in seconds, 60-3600. Default 300. code string optional Your own code, 4-10 letters or digits. Otherwise Udeyou generates a numeric code. template string optional Plain text up to 1000 characters. Must contain {{code}}; {{sender_name}} and {{minutes}} are also available. subject string optional Up to 120 characters, same placeholders. reply_to string optional Address that receives the customer's replies. Send code_length and expires_in as JSON numbers: "6" as a string is rejected with 422. Sending a new code to the same address cancels the previous ones. Every email ends with a line saying it was sent on behalf of sender_name through Udeyou OTP. data: otp_id, to, status ("sent"), mode ("live" or "test"), expires_in, expires_at (ISO 8601, UTC), credits_remaining, and test_mode + code with a test key (keep the code as a string: it may start with 0). POST /otp/verify -> 200 OK code string required The code the customer typed (4-10 letters or digits). otp_id string recommended The id returned by /otp/send. to string alternative Recipient email instead of otp_id; checks the latest code sent to it. data: verified (true), otp_id, to, verified_at. Use data.to as the verified address. A code works once, expires after expires_in seconds and is locked after 5 wrong attempts. GET /balance -> 200 OK data: company, mode, credits. GET /health -> 200 OK (no key needed) {"success": true, "status": "ok"} Success: {"success": true, "data": {...}} Failure: {"success": false, "error": {"code": "...", "message": "...", ...}} 6. Errors (use error.code in your logic; message is English help text) 400 invalid_json Body is not a JSON object. 400 invalid_code Wrong code. attempts_remaining = tries left. 401 invalid_api_key Key missing, malformed, unknown or revoked. 402 insufficient_credits Live key and a zero balance. 403 account_suspended The account is suspended. 404 otp_not_found No code matches otp_id or to for this key type. 404 not_found Unknown endpoint. 405 method_not_allowed HTTP method not supported for this endpoint. 409 otp_already_used The code was already used. 410 otp_expired Expired, or a newer code was sent to the same address. 422 validation_error Invalid fields (details in error.fields on /otp/send). 429 rate_limited More than 60 send requests per minute (retry_after). 429 resend_too_soon Less than 60 s since the last code to this address (retry_after). 429 recipient_limit 5 codes to this address in the last hour (retry_after). 429 too_many_attempts All 5 attempts used: request a new code. 500 server_error Unexpected server error. 502 delivery_failed The email could not be delivered; the credit was refunded. 7. Limits (per account, shared by live and test keys) - Send requests: 60 per minute (rate_limited, retry_after 60). - Gap between two codes to the same address: 60 seconds (resend_too_soon, retry_after 60). - Codes to the same address: 5 per hour (recipient_limit, retry_after 3600). - Wrong attempts: 5 per code (too_many_attempts, then request a new code). - Code lifetime: 300 seconds by default, 60-3600 with expires_in (otp_expired). Only /otp/send counts toward the per-minute limit. retry_after is the window length in seconds, not the time left: wait that long, then retry. 8. Best practices - Keep the key in an environment variable on the server only. - Use a test key in development and automated tests, a live key in production. - Store otp_id in the user session on the server and verify with it. - Trust data.to from the verify response, not an address sent by the app. - Treat the code as a string; it may start with 0. - Show attempts_remaining, and disable "resend" for 60 seconds after each send. - Set a timeout on every request and hide network errors from users. - Revoke a leaked key in the dashboard, then create a new one. - Never log codes or keys. Support: https://udeyou.com/contact (include the otp_id and the error code)