After payment confirmation, users often face access delays due to asynchronous payment processing. Race conditions between webhooks and redirect sessions cause status desynchronization — the enrollment is not created even though the payment succeeded. On 20+ projects, we solved this with idempotent webhook handlers and queues. Average access time is 2.3 seconds, and the reduction in cancellations is up to 25% due to instant access after verification. Integration cost is calculated individually and is typically 15–20% below market average because we use proven components. We ensure your content is protected and users get access without delays.
How to Avoid Race Conditions in Access Granting
A typical mistake is granting synchronous access after a redirect from the payment gateway. The user clicks the button, lands on the success page, gets access — but the payment didn't go through. Access must only be issued via webhook, otherwise the business loses revenue. We guarantee correct logic: enrollment is created only after payment verification via Stripe webhook signature verification.
Architecture:
[User] → [Checkout] → [Payment Gateway] ↓ [Webhook Handler] ↓ [Enrollment Service] → [DB: enrollments] ↓ [Access Control Layer] ↓ [LMS / Video Delivery / Downloads] Critical is the webhook handler — it creates the access record after payment confirmation. Idempotency: the enrollment record is created with a unique index on (user_id, course_id), a second call simply returns the existing record.
Comparison of Payment Gateways: Stripe vs YooKassa
| Gateway | Webhook processing time | Duplicate protection | Relative integration cost |
|---|---|---|---|
| Stripe | ~1 sec | Built-in idempotency | Baseline |
| YooKassa | ~2–3 sec | Via unique payment key | 30% higher |
Stripe's webhook processing is ~2 times faster than YooKassa's, which is critical under high load. For the Russian market, YooKassa is preferable due to localization and support for popular payment methods.
Why Signed URLs Don't Guarantee Security Without Proper Configuration
If the URL lifetime is too long or not tied to a specific user, content can leak. We use signed URLs with 2-hour expiration and IP binding. For premium courses, we use Mux with adaptive streaming (HLS) and signed playback tokens.
$url = $s3->createPresignedRequest( $s3->getCommand('GetObject', [ 'Bucket' => 'courses-bucket', 'Key' => "courses/{$courseId}/lesson-{$lessonId}.mp4", ]), '+2 hours' )->getUri(); For more details on signed URLs, refer to the official AWS documentation.
Access and Progress Management
Table enrollments:
| Field | Type | Description |
|---|---|---|
| id | uuid | Primary key |
| user_id | bigint | FK → users |
| course_id | bigint | FK → courses |
| payment_id | varchar | Transaction ID from gateway |
| expires_at | timestamp | NULL = unlimited |
| status | enum | active / suspended / refunded |
| created_at | timestamp | Purchase date |
Middleware on every protected route checks for an active record:
// Laravel Gate Gate::define('access-course', function (User $user, Course $course) { return $user->enrollments() ->where('course_id', $course->id) ->where('status', 'active') ->where(function ($q) { $q->whereNull('expires_at') ->orWhere('expires_at', '>', now()); }) ->exists(); }); Learning progress is tracked via lesson_progress(user_id, lesson_id, completed_at, watch_percent). A certificate is generated automatically when watch_percent >= 80 for all lessons in the course.
Additional Mechanisms
Free Preview (trial access)
The first 1–2 lessons are typically open without payment. A flag `is_free_preview` at the lesson level, checked in middleware. If the lesson is not free and there is no enrollment, the user is redirected to the purchase page.Refunds
On a refund, Stripe sends `charge.refunded`. We change `enrollments.status = 'refunded'`, and the user loses access instantly. For YooKassa, a similar webhook `payment.canceled` is used.Monitoring and Testing
To ensure reliability, we set up monitoring of webhook notifications: every failure is logged, and the team receives an alert in Telegram. This allows us to react to issues before they impact users. Before launch, we perform load testing — we create 100+ parallel payments and verify that all enrollments are created correctly without race conditions.
What's Included in the Result
- Payment gateway integration (Stripe, YooKassa, Robokassa) with webhook handling
- Enrollment system with access check via middleware
- Secure video delivery (signed URLs / Mux)
- Progress tracking with watch percentage persistence
- PDF certificate generation
- Admin dashboard for managing enrollments and refunds
- API documentation and operations manual
- Technical support during launch
- Webhook monitoring and alerts on failures
Implementation Timelines
| Stage | Time |
|---|---|
| Basic Stripe integration + enrollment | 2–3 days |
| Secure video delivery (S3 signed URL) | 1–2 days |
| Progress tracking + UI progress bar | 1–2 days |
| Certificates (PDF generation) | 1 day |
| YooKassa / second gateway integration | 1–2 days |
| Admin dashboard (enrollments, revenue) | 2–3 days |
Total minimum viable version — 7–10 business days turnkey. Order payment integration for your course — it will increase conversion and reduce support load. Get a consultation on choosing a payment gateway and access architecture.







