[{"data":1,"prerenderedAt":15670},["ShallowReactive",2],{"navigation":3,"/blog/sportstech/beatbuddy-replay-video-analysis-app-for-swimmers-flutter-post":734,"/blog/sportstech/beatbuddy-replay-video-analysis-app-for-swimmers-flutter-surround":2096,"/blog/sportstech/beatbuddy-replay-video-analysis-app-for-swimmers-flutter-related":2101},[4,70,207,312,721],{"title":5,"path":6,"stem":7,"children":8,"page":69},"Case Study","/blog/case-study","blog/case-study",[9,13,17,21,25,29,33,37,41,45,49,53,57,61,65],{"title":10,"path":11,"stem":12},"Ambistream – Multi-Layer Streaming Platform","/blog/case-study/ambistream-building-a-multi-layer-streaming-platform-from-a-spark-of-an-idea","blog/case-study/ambistream-building-a-multi-layer-streaming-platform-from-a-spark-of-an-idea",{"title":14,"path":15,"stem":16},"Custom Chromecast & AirPlay Casting App","/blog/case-study/chromecast-airplay-casting-app-case-study","blog/case-study/chromecast-airplay-casting-app-case-study",{"title":18,"path":19,"stem":20},"Direct Music Licensing Platform Case Study","/blog/case-study/dlm-music-catalog-case-study","blog/case-study/dlm-music-catalog-case-study",{"title":22,"path":23,"stem":24},"Assembly Instructions App - Galeco Mobile App","/blog/case-study/galeco-mobile-app-case-study","blog/case-study/galeco-mobile-app-case-study",{"title":26,"path":27,"stem":28},"MemoSonic: Building an Audio Memory Game","/blog/case-study/how-we-built-memosonic-accessible-audio-memory-game-flutter","blog/case-study/how-we-built-memosonic-accessible-audio-memory-game-flutter",{"title":30,"path":31,"stem":32},"Loyalty Program App for Shopping Mall","/blog/case-study/loyalty-program-application-case-study","blog/case-study/loyalty-program-application-case-study",{"title":34,"path":35,"stem":36},"Mobile Application for Music Catalogs","/blog/case-study/mobile-app-for-music-catalog","blog/case-study/mobile-app-for-music-catalog",{"title":38,"path":39,"stem":40},"Universal Music Data Parser for 20+ Platforms","/blog/case-study/musicdata-lab-universal-music-data-parser-case-study","blog/case-study/musicdata-lab-universal-music-data-parser-case-study",{"title":42,"path":43,"stem":44},"Panther ML/AI Pricing Recommendation Tool","/blog/case-study/panther-pricing-recommendation-tool-case-study","blog/case-study/panther-pricing-recommendation-tool-case-study",{"title":46,"path":47,"stem":48},"4D Grupa Roofing Wholesalers Platform","/blog/case-study/roofing-wholesalers-website-case-study","blog/case-study/roofing-wholesalers-website-case-study",{"title":50,"path":51,"stem":52},"Talent Alpha HR Frontend Platform","/blog/case-study/talent-alpha-hr-platform-case-study","blog/case-study/talent-alpha-hr-platform-case-study",{"title":54,"path":55,"stem":56},"Ticketing & Events Platform Development","/blog/case-study/ticketing-events-platform-case-study","blog/case-study/ticketing-events-platform-case-study",{"title":58,"path":59,"stem":60},"Turn Fans into Superfans — Roadie.co","/blog/case-study/turn-fans-into-superfans-roadie-co","blog/case-study/turn-fans-into-superfans-roadie-co",{"title":62,"path":63,"stem":64},"Walkative 2.0 Global Booking Engine","/blog/case-study/walkative-2-booking-platform-case-study","blog/case-study/walkative-2-booking-platform-case-study",{"title":66,"path":67,"stem":68},"Why Merch Is the New Royalty Check, and How Tech Is Closing the Gap","/blog/case-study/why-merch-is-the-new-royalty-check","blog/case-study/why-merch-is-the-new-royalty-check",false,{"title":71,"path":72,"stem":73,"children":74,"page":69},"Music Data","/blog/music-data","blog/music-data",[75,79,83,87,91,95,99,103,107,111,115,119,123,127,131,135,139,143,147,151,155,159,163,167,171,175,179,183,187,191,195,199,203],{"title":76,"path":77,"stem":78},"13 Distributors, 5 File Formats, Zero Standards -The Reality of Music Royalty Data","/blog/music-data/13-distributors-5-file-formats-zero-standards-the-reality-of-music-royalty-data","blog/music-data/13-distributors-5-file-formats-zero-standards-the-reality-of-music-royalty-data",{"title":80,"path":81,"stem":82},"830 Ways to Say Spotify - Normalizing Music Streaming Data","/blog/music-data/830-ways-to-say-spotify-normalizing-music-streaming-data","blog/music-data/830-ways-to-say-spotify-normalizing-music-streaming-data",{"title":84,"path":85,"stem":86},"Why Music Companies Need AI-Powered Analytics (And How We Built One)","/blog/music-data/ai-powered-analytics-dashboard-django-clickhouse-ollama","blog/music-data/ai-powered-analytics-dashboard-django-clickhouse-ollama",{"title":88,"path":89,"stem":90},"AI Rehearsal: Spaced Repetition for Your Musical Ideas","/blog/music-data/ai-rehearsal-spaced-repetition-for-musical-ideas","blog/music-data/ai-rehearsal-spaced-repetition-for-musical-ideas",{"title":92,"path":93,"stem":94},"Audio Project Organization Is a Mess — Here's Why","/blog/music-data/audio-project-organization-mess","blog/music-data/audio-project-organization-mess",{"title":96,"path":97,"stem":98},"Why Audio Search Is Still Broken and How to Fix It with Embeddings","/blog/music-data/audio-search-broken-fix-with-embeddings","blog/music-data/audio-search-broken-fix-with-embeddings",{"title":100,"path":101,"stem":102},"AI Song Structure Analysis: Intro, Verse, Chorus","/blog/music-data/automatic-song-structure-analysis-how-ai-detects-intro-verse-chorus","blog/music-data/automatic-song-structure-analysis-how-ai-detects-intro-verse-chorus",{"title":104,"path":105,"stem":106},"The Broken Feedback Loop in Music Collaboration","/blog/music-data/broken-feedback-loop-music-collaboration","blog/music-data/broken-feedback-loop-music-collaboration",{"title":108,"path":109,"stem":110},"Building a Claude Skill for DDEX Validation: Automate Music Metadata Checks with AI","/blog/music-data/building-a-claude-skill-for-ddex-validation-music-metadata","blog/music-data/building-a-claude-skill-for-ddex-validation-music-metadata",{"title":112,"path":113,"stem":114},"Building a Custom Music Delivery Platform on the Revelator API","/blog/music-data/building-a-custom-music-delivery-platform-on-the-revelator-api","blog/music-data/building-a-custom-music-delivery-platform-on-the-revelator-api",{"title":116,"path":117,"stem":118},"Building a Suno AI Remix App with Nuxt & Firebase","/blog/music-data/building-a-suno-remix-app-with-nuxt-and-firebase","blog/music-data/building-a-suno-remix-app-with-nuxt-and-firebase",{"title":120,"path":121,"stem":122},"C2PA & DDEX: Authenticity Meets Rights in the Age of AI Music","/blog/music-data/c2pa-and-ddex-authenticity-meets-rights-in-the-age-of-ai-music","blog/music-data/c2pa-and-ddex-authenticity-meets-rights-in-the-age-of-ai-music",{"title":124,"path":125,"stem":126},"C2PA in Music: A Claude MCP for Reading Content Provenance","/blog/music-data/c2pa-in-music-mcp","blog/music-data/c2pa-in-music-mcp",{"title":128,"path":129,"stem":130},"Data Modeling in MongoDB Using Design Patterns","/blog/music-data/data-modeling-in-mongodb-with-the-usage-of-design-patterns","blog/music-data/data-modeling-in-mongodb-with-the-usage-of-design-patterns",{"title":132,"path":133,"stem":134},"Office Hours with MusicTech Lab's DDEX Expert","/blog/music-data/ddex-office-hours-musictech","blog/music-data/ddex-office-hours-musictech",{"title":136,"path":137,"stem":138},"DDEX Open Source Projects Review","/blog/music-data/ddex-open-source-projects-review","blog/music-data/ddex-open-source-projects-review",{"title":140,"path":141,"stem":142},"Extracting Data from Ableton .als and .asd Files","/blog/music-data/extracting-data-from-ableton-als-asd-files","blog/music-data/extracting-data-from-ableton-als-asd-files",{"title":144,"path":145,"stem":146},"Maciej Dulski on Sound Connections Podcast","/blog/music-data/from-startups-to-musictech-maciej-dulski-on-sound-connections-podcast","blog/music-data/from-startups-to-musictech-maciej-dulski-on-sound-connections-podcast",{"title":148,"path":149,"stem":150},"How to Transcribe Video to Text Using OpenAI Whisper","/blog/music-data/how-to-transcribe-video-to-text-using-whisper","blog/music-data/how-to-transcribe-video-to-text-using-whisper",{"title":152,"path":153,"stem":154},"Epidemic Sound MCP with Claude for Devs","/blog/music-data/how-to-use-epidemic-sound-mcp-with-claude","blog/music-data/how-to-use-epidemic-sound-mcp-with-claude",{"title":156,"path":157,"stem":158},"Hybrid Database Model in Django for Speed","/blog/music-data/hybrid-database-model-in-django-as-a-performance-booster","blog/music-data/hybrid-database-model-in-django-as-a-performance-booster",{"title":160,"path":161,"stem":162},"Introduction to generating DDEX file using Python","/blog/music-data/introduction-to-generating-ddex-file-using-python","blog/music-data/introduction-to-generating-ddex-file-using-python",{"title":164,"path":165,"stem":166},"Maintaining Music Tech Tools: The SLA Dilemma for Small Teams","/blog/music-data/maintaining-music-tech-tools-the-sla-dilemma-for-small-teams","blog/music-data/maintaining-music-tech-tools-the-sla-dilemma-for-small-teams",{"title":168,"path":169,"stem":170},"Querying Bandcamp Revenue Reports with Natural Language — Meet mtl-bandcamp-mcp","/blog/music-data/mtl-bandcamp-mcp-open-source-revenue-dashboard","blog/music-data/mtl-bandcamp-mcp-open-source-revenue-dashboard",{"title":172,"path":173,"stem":174},"mtl-metadata-mcp: Open Source Audio Metadata Embedding for Claude Code","/blog/music-data/mtl-metadata-mcp-open-source-audio-metadata-embedding","blog/music-data/mtl-metadata-mcp-open-source-audio-metadata-embedding",{"title":176,"path":177,"stem":178},"MusicTech Resources for Builders","/blog/music-data/musictech-resources-curated-insights-for-the-musictech-builders","blog/music-data/musictech-resources-curated-insights-for-the-musictech-builders",{"title":180,"path":181,"stem":182},"Poland's Creative Tech and MusicTech Rise","/blog/music-data/polands-creative-tech-sector-is-on-the-rise-and-musictech-is-part-of-it","blog/music-data/polands-creative-tech-sector-is-on-the-rise-and-musictech-is-part-of-it",{"title":184,"path":185,"stem":186},"Batch ISRC Enrichment That Turns Messy Catalogs Into Clean Data","/blog/music-data/scout-isrc-metadata-enrichment-spotify-musicbrainz","blog/music-data/scout-isrc-metadata-enrichment-spotify-musicbrainz",{"title":188,"path":189,"stem":190},"Music Self-Publishing: The Emuze.me Story","/blog/music-data/self-publishing-in-the-music-industry-a-tale-of-emuze-me","blog/music-data/self-publishing-in-the-music-industry-a-tale-of-emuze-me",{"title":192,"path":193,"stem":194},"Understanding the API First Approach","/blog/music-data/understanding-the-api-first-approach","blog/music-data/understanding-the-api-first-approach",{"title":196,"path":197,"stem":198},"The Voice Memo Graveyard Problem","/blog/music-data/voice-memo-graveyard-problem","blog/music-data/voice-memo-graveyard-problem",{"title":200,"path":201,"stem":202},"Which Database for Music Data? Redshift vs BigQuery vs ClickHouse and When to Use Each","/blog/music-data/which-database-for-music-data-redshift-vs-bigquery-vs-clickhouse","blog/music-data/which-database-for-music-data-redshift-vs-bigquery-vs-clickhouse",{"title":204,"path":205,"stem":206},"Why we decided to use wavesurfer.js","/blog/music-data/why-we-decided-to-use-wavesurfer","blog/music-data/why-we-decided-to-use-wavesurfer",{"title":208,"path":209,"stem":210,"children":211,"page":69},"Newsletter","/blog/newsletter","blog/newsletter",[212,216,220,224,228,232,236,240,244,248,252,256,260,264,268,272,276,280,284,288,292,296,300,304,308],{"title":213,"path":214,"stem":215},"Music Industry Tech Openings (April 2024 Update)","/blog/newsletter/music-industry-tech-openings-april-2024-update","blog/newsletter/music-industry-tech-openings-april-2024-update",{"title":217,"path":218,"stem":219},"Music Industry Tech Openings (April 2025 Update)","/blog/newsletter/music-industry-tech-openings-april-2025-update","blog/newsletter/music-industry-tech-openings-april-2025-update",{"title":221,"path":222,"stem":223},"Music Industry Tech Openings (August 2024 Update)","/blog/newsletter/music-industry-tech-openings-august-2024-update","blog/newsletter/music-industry-tech-openings-august-2024-update",{"title":225,"path":226,"stem":227},"Music Industry Tech Openings (December 2024 Update)","/blog/newsletter/music-industry-tech-openings-december-2024-update","blog/newsletter/music-industry-tech-openings-december-2024-update",{"title":229,"path":230,"stem":231},"Music Industry Tech Openings (February 2025 Update)","/blog/newsletter/music-industry-tech-openings-february-2025-update","blog/newsletter/music-industry-tech-openings-february-2025-update",{"title":233,"path":234,"stem":235},"Music Industry Tech Openings (January 2025 Update)","/blog/newsletter/music-industry-tech-openings-january-2025-update","blog/newsletter/music-industry-tech-openings-january-2025-update",{"title":237,"path":238,"stem":239},"Music Industry Tech Openings (July 2024 Update)","/blog/newsletter/music-industry-tech-openings-july-2024-update","blog/newsletter/music-industry-tech-openings-july-2024-update",{"title":241,"path":242,"stem":243},"Music Industry Tech Openings (June 2024 Update)","/blog/newsletter/music-industry-tech-openings-june-2024-update","blog/newsletter/music-industry-tech-openings-june-2024-update",{"title":245,"path":246,"stem":247},"Music Industry Tech Openings (March 2025 Update)","/blog/newsletter/music-industry-tech-openings-march-2025-update","blog/newsletter/music-industry-tech-openings-march-2025-update",{"title":249,"path":250,"stem":251},"Music Industry Tech Openings (May 2024 Update)","/blog/newsletter/music-industry-tech-openings-may-2024-update","blog/newsletter/music-industry-tech-openings-may-2024-update",{"title":253,"path":254,"stem":255},"Music Industry Tech Openings (May 2025 Update)","/blog/newsletter/music-industry-tech-openings-may-2025","blog/newsletter/music-industry-tech-openings-may-2025",{"title":257,"path":258,"stem":259},"Music Industry Tech Openings (November 2024 Update)","/blog/newsletter/music-industry-tech-openings-november-2024-update","blog/newsletter/music-industry-tech-openings-november-2024-update",{"title":261,"path":262,"stem":263},"Music Industry Tech Openings (October 2024 Update)","/blog/newsletter/music-industry-tech-openings-october-2024-update","blog/newsletter/music-industry-tech-openings-october-2024-update",{"title":265,"path":266,"stem":267},"Music Industry Tech Openings (September 2024 Update)","/blog/newsletter/music-industry-tech-openings-september-2024-update","blog/newsletter/music-industry-tech-openings-september-2024-update",{"title":269,"path":270,"stem":271},"MusicTech Insights #1 by Maciej Dulski","/blog/newsletter/musictech-insights-1-curated-by-maciej-dulski","blog/newsletter/musictech-insights-1-curated-by-maciej-dulski",{"title":273,"path":274,"stem":275},"Provenance, Not Detection, Is the Durable Answer","/blog/newsletter/musictech-insights-10-curated-by-jim-anderson","blog/newsletter/musictech-insights-10-curated-by-jim-anderson",{"title":277,"path":278,"stem":279},"What CMOs can teach us about innovation in uncertain times","/blog/newsletter/musictech-insights-10-curated-by-joanna-kurkowska","blog/newsletter/musictech-insights-10-curated-by-joanna-kurkowska",{"title":281,"path":282,"stem":283},"Feeling the MusicTech Momentum","/blog/newsletter/musictech-insights-2-curated-by-maciej-dulski","blog/newsletter/musictech-insights-2-curated-by-maciej-dulski",{"title":285,"path":286,"stem":287},"AI in Music: Hype, Hope, and a Human Touch","/blog/newsletter/musictech-insights-3-curated-by-drew-thurlow","blog/newsletter/musictech-insights-3-curated-by-drew-thurlow",{"title":289,"path":290,"stem":291},"The Music Metadata Conundrum","/blog/newsletter/musictech-insights-4-curated-by-amanda-schupf","blog/newsletter/musictech-insights-4-curated-by-amanda-schupf",{"title":293,"path":294,"stem":295},"7 Rounds in the First 10 Days of November 2025","/blog/newsletter/musictech-insights-5-curated-by-maciej-dulski","blog/newsletter/musictech-insights-5-curated-by-maciej-dulski",{"title":297,"path":298,"stem":299},"The End of an Era: It's All About to Crash","/blog/newsletter/musictech-insights-6-curated-by-sigurdur-arnason","blog/newsletter/musictech-insights-6-curated-by-sigurdur-arnason",{"title":301,"path":302,"stem":303},"Low-Code Magic Won't Solve MusicTech Reality","/blog/newsletter/musictech-insights-7-curated-by-mariusz-smenzyk","blog/newsletter/musictech-insights-7-curated-by-mariusz-smenzyk",{"title":305,"path":306,"stem":307},"The New Economics of Game Music","/blog/newsletter/musictech-insights-8-curated-by-kenny-vaughan","blog/newsletter/musictech-insights-8-curated-by-kenny-vaughan",{"title":309,"path":310,"stem":311},"Music Business Meets Direct-to-Fan","/blog/newsletter/musictech-insights-9-curated-by-yaw-asamani","blog/newsletter/musictech-insights-9-curated-by-yaw-asamani",{"title":313,"path":314,"stem":315,"children":316,"page":69},"Software Development","/blog/software-development","blog/software-development",[317,321,325,329,333,337,341,345,349,353,357,361,365,369,373,377,381,385,389,393,397,401,405,409,413,417,421,425,429,433,437,441,445,449,453,457,461,465,469,473,477,481,485,489,493,497,501,505,509,513,517,521,525,529,533,537,541,545,549,553,557,561,565,569,573,577,581,585,589,593,597,601,605,609,613,617,621,625,629,633,637,641,645,649,653,657,661,665,669,673,677,681,685,689,693,697,701,705,709,713,717],{"title":318,"path":319,"stem":320},"Benefits of Outsourcing Software Development","/blog/software-development/10-benefits-of-outsourcing-software-development-services","blog/software-development/10-benefits-of-outsourcing-software-development-services",{"title":322,"path":323,"stem":324},"10 Steps to Find the Best MVP Developers","/blog/software-development/10-steps-to-find-the-best-mvp-developers-for-your-startup-idea","blog/software-development/10-steps-to-find-the-best-mvp-developers-for-your-startup-idea",{"title":326,"path":327,"stem":328},"1,200 Looms Later: How Async Video Became My Development Superpower","/blog/software-development/1200-looms-how-async-video-became-our-development-superpower","blog/software-development/1200-looms-how-async-video-became-our-development-superpower",{"title":330,"path":331,"stem":332},"Communication Strategy in Outsourcing Projects","/blog/software-development/5-steps-to-implement-an-effective-communication-strategy-in-outsourcing-software-development-project","blog/software-development/5-steps-to-implement-an-effective-communication-strategy-in-outsourcing-software-development-project",{"title":334,"path":335,"stem":336},"7 Best Practices for Outsourcing Software Development","/blog/software-development/7-best-practices-for-outsourcing-software-development","blog/software-development/7-best-practices-for-outsourcing-software-development",{"title":338,"path":339,"stem":340},"9 Reasons Why Saleor.io Is Best for eCommerce","/blog/software-development/9-reasons-why-the-saleor-io-platform-is-the-best-choice-for-your-ecommerce-website","blog/software-development/9-reasons-why-the-saleor-io-platform-is-the-best-choice-for-your-ecommerce-website",{"title":342,"path":343,"stem":344},"A Look at Bravelab.io’s Clutch 2021 Year In Review","/blog/software-development/a-look-at-bravelab-ios-clutch-2021-year-in-review","blog/software-development/a-look-at-bravelab-ios-clutch-2021-year-in-review",{"title":346,"path":347,"stem":348},"A quick introduction to profit sharing implementation","/blog/software-development/a-quick-introduction-to-profit-sharing-implementation","blog/software-development/a-quick-introduction-to-profit-sharing-implementation",{"title":350,"path":351,"stem":352},"AI Audio Similarity Search: The Future of Sound Library Discovery","/blog/software-development/ai-audio-similarity-search-for-sound-libraries","blog/software-development/ai-audio-similarity-search-for-sound-libraries",{"title":354,"path":355,"stem":356},"Automate Repetitive Tasks for Better Results","/blog/software-development/automate-repetitive-tasks-to-improve-your-business-performance","blog/software-development/automate-repetitive-tasks-to-improve-your-business-performance",{"title":358,"path":359,"stem":360},"Automating Success: The Art of Unified Documentation","/blog/software-development/automating-success-the-art-of-unified-documentation","blog/software-development/automating-success-the-art-of-unified-documentation",{"title":362,"path":363,"stem":364},"Brave 3.0 Website Redesign, Part 2: Solution","/blog/software-development/brave-3-0-how-we-conducted-website-redesign-part-2-solution","blog/software-development/brave-3-0-how-we-conducted-website-redesign-part-2-solution",{"title":366,"path":367,"stem":368},"Brave 3.0, Part 4: Tech Stack and Recap","/blog/software-development/brave-3-0-part-4-technologies-behind-and-final-series-recap","blog/software-development/brave-3-0-part-4-technologies-behind-and-final-series-recap",{"title":370,"path":371,"stem":372},"Brave 3.0 – redesign process part 1. The Challenge","/blog/software-development/brave-3-0-redesign-process-part-1-challenge","blog/software-development/brave-3-0-redesign-process-part-1-challenge",{"title":374,"path":375,"stem":376},"Brave 3.0 – redesign process, part 3. Lesson learned","/blog/software-development/brave-3-0-redesign-process-part-3-lesson-learned","blog/software-development/brave-3-0-redesign-process-part-3-lesson-learned",{"title":378,"path":379,"stem":380},"Bravelab.io: Top Software Developer by Clutch","/blog/software-development/bravelab-io-is-recognized-as-a-top-custom-software-developer-by-clutch","blog/software-development/bravelab-io-is-recognized-as-a-top-custom-software-developer-by-clutch",{"title":382,"path":383,"stem":384},"Bravelab.io: Top Developer in Poland by Clutch","/blog/software-development/bravelab-io-named-top-software-developer-in-poland-by-clutch","blog/software-development/bravelab-io-named-top-software-developer-in-poland-by-clutch",{"title":386,"path":387,"stem":388},"MusicTech Lab Partners with LALAL.AI","/blog/software-development/bravelab-partners-with-the-audio-lalal-ai","blog/software-development/bravelab-partners-with-the-audio-lalal-ai",{"title":390,"path":391,"stem":392},"MusicTech Lab Partners with The Audio Programmer","/blog/software-development/bravelab-partners-with-the-audio-programmer","blog/software-development/bravelab-partners-with-the-audio-programmer",{"title":394,"path":395,"stem":396},"Bravelab's team about productivity","/blog/software-development/bravelabs-team-about-productivity","blog/software-development/bravelabs-team-about-productivity",{"title":398,"path":399,"stem":400},"Braveloper","/blog/software-development/braveloper","blog/software-development/braveloper",{"title":402,"path":403,"stem":404},"Bravely App: Boost Productivity with Django","/blog/software-development/bravely-app-how-to-be-more-productive-with-django-quick","blog/software-development/bravely-app-how-to-be-more-productive-with-django-quick",{"title":406,"path":407,"stem":408},"DIY MIDI Controller for Ableton with Arduino","/blog/software-development/building-a-diy-midi-controller-for-ableton-live-with-arduino","blog/software-development/building-a-diy-midi-controller-for-ableton-live-with-arduino",{"title":410,"path":411,"stem":412},"C2PA in Ableton: Making AI Music Provenance Visible Inside Your DAW","/blog/software-development/c2pa-in-ableton-max-for-live","blog/software-development/c2pa-in-ableton-max-for-live",{"title":414,"path":415,"stem":416},"Change Detection mechanism in Angular","/blog/software-development/change-detection-mechanism-in-angular","blog/software-development/change-detection-mechanism-in-angular",{"title":418,"path":419,"stem":420},"Communication Channels in Remote Work","/blog/software-development/comparison-of-the-communication-channels-in-remote-work","blog/software-development/comparison-of-the-communication-channels-in-remote-work",{"title":422,"path":423,"stem":424},"Connecting Your Max for Live Device to a Cloud API","/blog/software-development/connecting-your-max-for-live-device-to-a-cloud-api","blog/software-development/connecting-your-max-for-live-device-to-a-cloud-api",{"title":426,"path":427,"stem":428},"From Voice Memo to Studio: The Cross-Platform Problem for Creators","/blog/software-development/cross-platform-problem-for-creators","blog/software-development/cross-platform-problem-for-creators",{"title":430,"path":431,"stem":432},"Cultural transformation through the pandemic era","/blog/software-development/cultural-transformation-through-the-pandemic-era","blog/software-development/cultural-transformation-through-the-pandemic-era",{"title":434,"path":435,"stem":436},"D-Commerce Decoded: Cutting Through the Hype","/blog/software-development/d-commerce-decoded-cutting-through-the-hype","blog/software-development/d-commerce-decoded-cutting-through-the-hype",{"title":438,"path":439,"stem":440},"Dev Meeting 002: Intro to DDD","/blog/software-development/dev-meeting-002-introduction-to-domain-driven-design-ddd","blog/software-development/dev-meeting-002-introduction-to-domain-driven-design-ddd",{"title":442,"path":443,"stem":444},"Dev Meeting 003: Web3 Primer","/blog/software-development/dev-meeting-003-web3-primer","blog/software-development/dev-meeting-003-web3-primer",{"title":446,"path":447,"stem":448},"Dev Meeting 004: Introduction to Event Storming","/blog/software-development/dev-meeting-004-introduction-to-event-storming","blog/software-development/dev-meeting-004-introduction-to-event-storming",{"title":450,"path":451,"stem":452},"Dev Meeting 001: Kubernetes is a Framework","/blog/software-development/dev-meeting-kubernetes-is-a-framework","blog/software-development/dev-meeting-kubernetes-is-a-framework",{"title":454,"path":455,"stem":456},"Did You Know? 10 Developer Tips from Real Codebases","/blog/software-development/did-you-know-dev-tips-part-1","blog/software-development/did-you-know-dev-tips-part-1",{"title":458,"path":459,"stem":460},"10 Surprising MusicTech Facts (Part 2)","/blog/software-development/did-you-know-musictech-facts-part-2","blog/software-development/did-you-know-musictech-facts-part-2",{"title":462,"path":463,"stem":464},"Django-cms and GraphQL","/blog/software-development/django-cms-and-graphql","blog/software-development/django-cms-and-graphql",{"title":466,"path":467,"stem":468},"Does Zappa make it super easy?","/blog/software-development/does-zappa-make-it-super-easy","blog/software-development/does-zappa-make-it-super-easy",{"title":470,"path":471,"stem":472},"Establishing cooperation between Netlify and Bravelab","/blog/software-development/establishing-cooperation-between-netlify-and-bravelab","blog/software-development/establishing-cooperation-between-netlify-and-bravelab",{"title":474,"path":475,"stem":476},"Export Ableton Locators to JSON via Max for Live","/blog/software-development/exporting-ableton-live-locators-to-json-with-max-for-live","blog/software-development/exporting-ableton-live-locators-to-json-with-max-for-live",{"title":478,"path":479,"stem":480},"IT Outsourcing: Success and Failure Factors","/blog/software-development/factors-that-contribute-to-the-success-or-failure-of-an-it-outsourcing-project","blog/software-development/factors-that-contribute-to-the-success-or-failure-of-an-it-outsourcing-project",{"title":482,"path":483,"stem":484},"Flutter 2022 Strategy: Analyzing the Roadmap","/blog/software-development/flutter-strategy-for-2022-analyzing-the-new-flutter-roadmap","blog/software-development/flutter-strategy-for-2022-analyzing-the-new-flutter-roadmap",{"title":486,"path":487,"stem":488},"Git Better #1 — Commit Message Convention","/blog/software-development/git-better-1-see-more-with-a-commit-message-convention","blog/software-development/git-better-1-see-more-with-a-commit-message-convention",{"title":490,"path":491,"stem":492},"Hasura in action. How to use it with Django","/blog/software-development/hasura-in-action","blog/software-development/hasura-in-action",{"title":494,"path":495,"stem":496},"Holacracy why and where we are","/blog/software-development/holacracy-why-and-where-we-are","blog/software-development/holacracy-why-and-where-we-are",{"title":498,"path":499,"stem":500},"How does JavaScript work","/blog/software-development/how-does-javascript-work","blog/software-development/how-does-javascript-work",{"title":502,"path":503,"stem":504},"How important is good UX/UI design?","/blog/software-development/how-important-is-good-ux-ui-design","blog/software-development/how-important-is-good-ux-ui-design",{"title":506,"path":507,"stem":508},"How repetitive tasks impact your business","/blog/software-development/how-repetitive-tasks-impact-your-business","blog/software-development/how-repetitive-tasks-impact-your-business",{"title":510,"path":511,"stem":512},"Becoming a Vue.js Dev: Do Paid Trials Work?","/blog/software-development/how-to-become-a-vue-js-developer-and-whether-paid-trials-in-it-work-out","blog/software-development/how-to-become-a-vue-js-developer-and-whether-paid-trials-in-it-work-out",{"title":514,"path":515,"stem":516},"How to Build an MVP in 6 Steps","/blog/software-development/how-to-build-a-minimum-viable-product-mvp-in-6-steps","blog/software-development/how-to-build-a-minimum-viable-product-mvp-in-6-steps",{"title":518,"path":519,"stem":520},"How to conduct workshops for creative industry?","/blog/software-development/how-to-conduct-workshops-for-creative-industry","blog/software-development/how-to-conduct-workshops-for-creative-industry",{"title":522,"path":523,"stem":524},"How to easily create form in Angular","/blog/software-development/how-to-easily-create-form-in-angular","blog/software-development/how-to-easily-create-form-in-angular",{"title":526,"path":527,"stem":528},"How to export orders in Saleor.io to XLSX file","/blog/software-development/how-to-export-orders-in-saleor-io-to-xlsx-file","blog/software-development/how-to-export-orders-in-saleor-io-to-xlsx-file",{"title":530,"path":531,"stem":532},"Handling High Loads on E-Commerce Platforms","/blog/software-development/how-to-handle-high-loads-on-e-commerce-platform-with-ease","blog/software-development/how-to-handle-high-loads-on-e-commerce-platform-with-ease",{"title":534,"path":535,"stem":536},"How to launch Saleor.io shop instance within 40h","/blog/software-development/how-to-launch-saleor-io-shop-instance-within-40h","blog/software-development/how-to-launch-saleor-io-shop-instance-within-40h",{"title":538,"path":539,"stem":540},"First Steps to Build a Business Relationship","/blog/software-development/how-to-make-the-first-step-to-establish-a-business-relationship","blog/software-development/how-to-make-the-first-step-to-establish-a-business-relationship",{"title":542,"path":543,"stem":544},"Multi-Tenant Apps with Django and Saleor.io","/blog/software-development/how-to-manage-tenants-in-the-multitenant-app-based-on-django-tenants-and-saleor-io-platform","blog/software-development/how-to-manage-tenants-in-the-multitenant-app-based-on-django-tenants-and-saleor-io-platform",{"title":546,"path":547,"stem":548},"Notion Backup Tool Built in 3 Days with Python","/blog/software-development/how-we-built-a-notion-backup-tool-in-3-days-with-pythonvue-and-why","blog/software-development/how-we-built-a-notion-backup-tool-in-3-days-with-pythonvue-and-why",{"title":550,"path":551,"stem":552},"Important new features in Python 3.8","/blog/software-development/important-new-features-in-python-3-8","blog/software-development/important-new-features-in-python-3-8",{"title":554,"path":555,"stem":556},"Installing Proxmox on dedicated server from OVH","/blog/software-development/installing-proxmox-on-dedicated-server-from-ovh","blog/software-development/installing-proxmox-on-dedicated-server-from-ovh",{"title":558,"path":559,"stem":560},"Integrating SignNow E-Signatures into Your Django Application","/blog/software-development/integrating-signnow-e-signatures-into-your-django-application","blog/software-development/integrating-signnow-e-signatures-into-your-django-application",{"title":562,"path":563,"stem":564},"Tempus Metronome and GetSongBPM API","/blog/software-development/integrating-tempus-metronome-with-the-getsongbpm-api-what-bpm-really-means-and-how-to-use-it","blog/software-development/integrating-tempus-metronome-with-the-getsongbpm-api-what-bpm-really-means-and-how-to-use-it",{"title":566,"path":567,"stem":568},"Introducing MusicTech Poland","/blog/software-development/introducing-musictech-poland","blog/software-development/introducing-musictech-poland",{"title":570,"path":571,"stem":572},"Vue.js as a Frontend for Saleor.io","/blog/software-development/is-it-possible-to-use-vue-js-as-a-frontend-for-saleor-io-platform","blog/software-development/is-it-possible-to-use-vue-js-as-a-frontend-for-saleor-io-platform",{"title":574,"path":575,"stem":576},"Is your business ready for the cashless era?","/blog/software-development/is-your-business-ready-for-the-cashless-era","blog/software-development/is-your-business-ready-for-the-cashless-era",{"title":578,"path":579,"stem":580},"Is your face ready to buy?","/blog/software-development/is-your-face-ready-to-buy","blog/software-development/is-your-face-ready-to-buy",{"title":582,"path":583,"stem":584},"JS Frameworks: Trends and Opportunities","/blog/software-development/javascript-trending-frameworks-and-market-opportunities","blog/software-development/javascript-trending-frameworks-and-market-opportunities",{"title":586,"path":587,"stem":588},"Kanban Board: Boost Your Team Productivity","/blog/software-development/kanban-board-methodology-hack-your-companys-productivity","blog/software-development/kanban-board-methodology-hack-your-companys-productivity",{"title":590,"path":591,"stem":592},"Verified Human Cert MCP Server: Prove Your Music Is Human-Made, Right from the Terminal","/blog/software-development/mcp-verified-human-cert-open-source","blog/software-development/mcp-verified-human-cert-open-source",{"title":594,"path":595,"stem":596},"Migrating from TravisCI to Github Actions","/blog/software-development/migrating-from-travisci-to-github-actions","blog/software-development/migrating-from-travisci-to-github-actions",{"title":598,"path":599,"stem":600},"MusicTech Lab: Top Software Developer by Clutch","/blog/software-development/musictechlab-is-recognized-as-a-top-custom-software-developer-by-clutch","blog/software-development/musictechlab-is-recognized-as-a-top-custom-software-developer-by-clutch",{"title":602,"path":603,"stem":604},"MusicTech Lab x Verified Human: Building a Trust Layer for Human-Made Music","/blog/software-development/musictechlab_blog_verified_human_partnership","blog/software-development/musictechlab_blog_verified_human_partnership",{"title":606,"path":607,"stem":608},"MusicXML: Standard for Music Notation","/blog/software-development/musicxml-standard-for-music-notation-and-education","blog/software-development/musicxml-standard-for-music-notation-and-education",{"title":610,"path":611,"stem":612},"Only a few books but dozens of ideas","/blog/software-development/only-a-few-books-but-dozens-of-ideas","blog/software-development/only-a-few-books-but-dozens-of-ideas",{"title":614,"path":615,"stem":616},"Overdue Invoices and Issue Tracker Integration","/blog/software-development/overdue-invoices-integration-with-the-issue-tracking-system","blog/software-development/overdue-invoices-integration-with-the-issue-tracking-system",{"title":618,"path":619,"stem":620},"Performing SAML SSO using JWT in Django","/blog/software-development/performing-saml-sso-using-jwt-in-django","blog/software-development/performing-saml-sso-using-jwt-in-django",{"title":622,"path":623,"stem":624},"Progressive Web Apps for Mobile Development","/blog/software-development/progressive-web-apps-a-new-way-of-creating-mobile-application","blog/software-development/progressive-web-apps-a-new-way-of-creating-mobile-application",{"title":626,"path":627,"stem":628},"Recruitment System: Gmail, Jira, and CRM","/blog/software-development/recruitment-system-integrating-gmail-bravely-jira-slack-and-copper-crm","blog/software-development/recruitment-system-integrating-gmail-bravely-jira-slack-and-copper-crm",{"title":630,"path":631,"stem":632},"Scratch Me: Chrome Extension for Leads","/blog/software-development/scratch-me-a-simple-chrome-extension-which-will-increase-your-productivity","blog/software-development/scratch-me-a-simple-chrome-extension-which-will-increase-your-productivity",{"title":634,"path":635,"stem":636},"Scratch Me – integration with the Copper CRM","/blog/software-development/scratch-me-integration-with-the-copper-crm","blog/software-development/scratch-me-integration-with-the-copper-crm",{"title":638,"path":639,"stem":640},"SignNow MCP Server: E-Signatures Straight from Claude Code","/blog/software-development/signnow-mcp-server-e-signatures-from-claude-code","blog/software-development/signnow-mcp-server-e-signatures-from-claude-code",{"title":642,"path":643,"stem":644},"Music Industry Tech Openings (March 2024 Update)","/blog/software-development/technical-job-opportunities-in-the-music-industry","blog/software-development/technical-job-opportunities-in-the-music-industry",{"title":646,"path":647,"stem":648},"Thanks app – a Management 3.0 solution","/blog/software-development/thanks-app-a-management-3-0-solution","blog/software-development/thanks-app-a-management-3-0-solution",{"title":650,"path":651,"stem":652},"Colonial Pipeline Case: 7 Security Reminders","/blog/software-development/the-case-of-colonial-pipeline-and-7-security-reminders","blog/software-development/the-case-of-colonial-pipeline-and-7-security-reminders",{"title":654,"path":655,"stem":656},"The Evolution and Future of E-commerce Platforms","/blog/software-development/the-evolution-and-future-of-e-commerce-platforms","blog/software-development/the-evolution-and-future-of-e-commerce-platforms",{"title":658,"path":659,"stem":660},"The Gender Gap in the Tech Industry","/blog/software-development/the-gender-gap-in-the-tech-industry","blog/software-development/the-gender-gap-in-the-tech-industry",{"title":662,"path":663,"stem":664},"First Attempt to Implement 4DX at Bravelab.io","/blog/software-development/the-very-first-attempt-to-implement-4dx-in-bravelab-io","blog/software-development/the-very-first-attempt-to-implement-4dx-in-bravelab-io",{"title":666,"path":667,"stem":668},"The WTF Scale: IT Project Complexity","/blog/software-development/the-wtf-programming-scale-measuring-it-project-complexity","blog/software-development/the-wtf-programming-scale-measuring-it-project-complexity",{"title":670,"path":671,"stem":672},"Top 10 articles through the eyes of our developers","/blog/software-development/top-10-articles-through-the-eyes-of-our-developers","blog/software-development/top-10-articles-through-the-eyes-of-our-developers",{"title":674,"path":675,"stem":676},"Top 6 apps made with Flutter","/blog/software-development/top-6-apps-made-with-flutter","blog/software-development/top-6-apps-made-with-flutter",{"title":678,"path":679,"stem":680},"Uber 101: How Uber Made It to the Top","/blog/software-development/uber-101-how-this-ride-sharing-behemoth-made-it-to-the-top","blog/software-development/uber-101-how-this-ride-sharing-behemoth-made-it-to-the-top",{"title":682,"path":683,"stem":684},"MusicTech Lab Partners with Music Glue","/blog/software-development/unifying-artists-and-audiences-exploring-music-glue","blog/software-development/unifying-artists-and-audiences-exploring-music-glue",{"title":686,"path":687,"stem":688},"Why AI Will Defeat Traditional HR","/blog/software-development/warning-why-artificial-intelligence-will-defeat-traditional-hr","blog/software-development/warning-why-artificial-intelligence-will-defeat-traditional-hr",{"title":690,"path":691,"stem":692},"What is a Discovery Document?","/blog/software-development/what-is-discovery-document","blog/software-development/what-is-discovery-document",{"title":694,"path":695,"stem":696},"What is Flutter, and Why is it Worth Considering?","/blog/software-development/what-is-flutter-and-why-is-it-worth-considering","blog/software-development/what-is-flutter-and-why-is-it-worth-considering",{"title":698,"path":699,"stem":700},"What is a Watermarked Song?","/blog/software-development/what-is-watermarked-song","blog/software-development/what-is-watermarked-song",{"title":702,"path":703,"stem":704},"Choosing a Frontend Framework for the Web","/blog/software-development/which-framework-should-you-choose-for-the-frontend-web-platform-development","blog/software-development/which-framework-should-you-choose-for-the-frontend-web-platform-development",{"title":706,"path":707,"stem":708},"Why DAWs Are the Wrong Tool for Starting a Song","/blog/software-development/why-daws-wrong-tool-for-starting-song","blog/software-development/why-daws-wrong-tool-for-starting-song",{"title":710,"path":711,"stem":712},"Why the Programming World Loves Python","/blog/software-development/why-the-programming-world-loves-python","blog/software-development/why-the-programming-world-loves-python",{"title":714,"path":715,"stem":716},"Why We Don't Build Chat From Scratch (And Neither Should You)","/blog/software-development/why-we-dont-build-chat-from-scratch","blog/software-development/why-we-dont-build-chat-from-scratch",{"title":718,"path":719,"stem":720},"Why we use Sanity.io","/blog/software-development/why-we-use-sanity-io","blog/software-development/why-we-use-sanity-io",{"title":722,"path":723,"stem":724,"children":725,"page":69},"Sportstech","/blog/sportstech","blog/sportstech",[726,730],{"title":727,"path":728,"stem":729},"BeatBuddy Replay: Video Analysis App Challenges","/blog/sportstech/beatbuddy-replay-video-analysis-app-for-swimmers-flutter","blog/sportstech/beatbuddy-replay-video-analysis-app-for-swimmers-flutter",{"title":731,"path":732,"stem":733},"How to Create a Watch Face App for Garmin Watch","/blog/sportstech/how-to-create-watch-face-app-for-garmin-watch","blog/sportstech/how-to-create-watch-face-app-for-garmin-watch",{"id":735,"title":727,"authors":736,"badge":742,"body":743,"category":2071,"client":742,"date":2072,"description":2073,"extension":2074,"faq":742,"featured":69,"featuredOrder":742,"hidden":69,"image":2075,"keyTakeaways":2077,"meta":2091,"navigation":1056,"path":728,"seo":2092,"status":742,"stem":729,"tags":2093,"teaser":742,"__hash__":2095},"posts/blog/sportstech/beatbuddy-replay-video-analysis-app-for-swimmers-flutter.md",[737],{"name":738,"to":739,"avatar":740},"Mariusz Smenżyk","https://www.linkedin.com/in/mariusz-smenzyk/",{"src":741},"/images/people/mariusz-smenzyk2.webp",null,{"type":744,"value":745,"toc":2046},"minimark",[746,751,763,766,769,773,778,794,798,821,825,833,837,848,850,854,857,863,869,875,881,884,886,890,893,897,900,903,909,913,916,928,984,987,992,996,999,1006,1247,1250,1255,1259,1262,1541,1544,1549,1553,1556,1637,1640,1645,1649,1652,1655,1660,1664,1667,1738,1741,1746,1750,1753,1756,1761,1765,1768,1837,1840,1845,1849,1852,1888,1891,1896,1898,1902,1905,1965,1967,1971,1974,1994,1996,2000,2006,2012,2018,2024,2027,2029,2042],[747,748,750],"h2",{"id":749},"introduction","Introduction",[752,753,754,758,759,762],"p",{},[755,756,757],"strong",{},"BeatBuddy Replay"," is a video analysis application designed for athletes, with a special focus on swimmers training with the Total Immersion method. It was originally named ",[755,760,761],{},"SwimLab Coach's Eye",". We are currently integrating it into the BeatBuddy ecosystem - family of products for athletes - which is why we decided to rename it.",[752,764,765],{},"The app was built with Flutter. It is currently available on iOS (iPhone and iPad), with an Android version in development.",[767,768],"hr",{},[747,770,772],{"id":771},"what-can-beatbuddy-replay-do","What Can BeatBuddy Replay Do?",[774,775,777],"h3",{"id":776},"video-playback","Video Playback",[779,780,781,785,788,791],"ul",{},[782,783,784],"li",{},"Frame-by-frame playback",[782,786,787],{},"Speed adjustment from 0.25x to 2x (slow motion)",[782,789,790],{},"Forward and backward scrubbing",[782,792,793],{},"Audio mute",[774,795,797],{"id":796},"drawing-tools","Drawing Tools",[779,799,800,803,806,809,812,815,818],{},[782,801,802],{},"Straight lines and arrows",[782,804,805],{},"Rectangles and circles",[782,807,808],{},"Triangles with automatic angle measurement",[782,810,811],{},"Curves (freehand drawing)",[782,813,814],{},"Protractor",[782,816,817],{},"Color and line thickness selection",[782,819,820],{},"Undo/Redo",[774,822,824],{"id":823},"analysis-and-focus","Analysis and Focus",[779,826,827,830],{},[782,828,829],{},"Mask mode (spotlight) - dims the background, allowing focus on a selected area",[782,831,832],{},"Swimming technique angle measurement",[774,834,836],{"id":835},"sharing","Sharing",[779,838,839,842,845],{},[782,840,841],{},"Screenshots with annotations",[782,843,844],{},"Analytical session recording",[782,846,847],{},"Sharing via system Share menu",[767,849],{},[747,851,853],{"id":852},"who-is-this-app-for","Who Is This App For?",[752,855,856],{},"BeatBuddy Replay was created for:",[752,858,859,862],{},[755,860,861],{},"Total Immersion Coaches"," - who need precise tools for technique analysis. The TI method is based on conscious work on every movement element, and the app allows coaches to show athletes exactly what and how to improve.",[752,864,865,868],{},[755,866,867],{},"Swimming Coaches"," - who want to show athletes their technical errors directly on the recording.",[752,870,871,874],{},[755,872,873],{},"Athletes"," - who independently analyze their training sessions and competitions.",[752,876,877,880],{},[755,878,879],{},"Swimming Clubs"," - looking for a simple video analysis tool without complicated software.",[752,882,883],{},"The app fills the gap between a simple video player and professional (and expensive) sports analysis software. It's intuitive, works offline, and requires no subscription.",[767,885],{},[747,887,889],{"id":888},"_10-technical-challenges","10 Technical Challenges",[752,891,892],{},"During two weeks of test development, we encountered many problems. Here are the 10 most important challenges and the lessons we learned from them.",[774,894,896],{"id":895},"_1-speed-vs-architecture-the-mvp-dilemma","1. Speed vs Architecture - The MVP Dilemma",[752,898,899],{},"All the application code is in a single file with over 3,300 lines. Without an advanced state management framework, everything relies on Flutter's basic mechanisms.",[752,901,902],{},"We wanted to deliver a working product to coaches who were waiting for this tool as quickly as possible. Perfect architecture can wait - user feedback cannot.",[752,904,905,908],{},[755,906,907],{},"Takeaway:"," For MVP projects, speed is more important than architecture, but refactoring will be essential for further development.",[774,910,912],{"id":911},"_2-ios-crashes-hidden-platform-requirements","2. iOS Crashes - Hidden Platform Requirements",[752,914,915],{},"One of the first serious problems was app crashes on iOS. The debugging process was tedious and required many attempts before we could locate the issue.",[752,917,918,919,923,924,927],{},"It turned out that plugins like ",[920,921,922],"code",{},"video_player"," or ",[920,925,926],{},"file_picker"," require appropriate entries in the iOS configuration:",[929,930,935],"pre",{"className":931,"code":932,"language":933,"meta":934,"style":934},"language-xml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003Ckey>NSPhotoLibraryUsageDescription\u003C/key>\n\u003Cstring>Needed to access videos for analysis\u003C/string>\n","xml","",[920,936,937,965],{"__ignoreMap":934},[938,939,942,946,950,953,957,960,962],"span",{"class":940,"line":941},"line",1,[938,943,945],{"class":944},"sMK4o","\u003C",[938,947,949],{"class":948},"swJcz","key",[938,951,952],{"class":944},">",[938,954,956],{"class":955},"sTEyZ","NSPhotoLibraryUsageDescription",[938,958,959],{"class":944},"\u003C/",[938,961,949],{"class":948},[938,963,964],{"class":944},">\n",[938,966,968,970,973,975,978,980,982],{"class":940,"line":967},2,[938,969,945],{"class":944},[938,971,972],{"class":948},"string",[938,974,952],{"class":944},[938,976,977],{"class":955},"Needed to access videos for analysis",[938,979,959],{"class":944},[938,981,972],{"class":948},[938,983,964],{"class":944},[752,985,986],{},"Every crash is a potentially lost user. A coach whose app crashes during a demonstration to an athlete won't give it a second chance.",[752,988,989,991],{},[755,990,907],{}," Cross-platform plugins often require platform-specific configuration that isn't obvious from the documentation.",[774,993,995],{"id":994},"_3-shape-drawing-building-a-custom-graphics-engine","3. Shape Drawing - Building a Custom Graphics Engine",[752,997,998],{},"From the first version of the app, we had to implement a complex drawing system supporting 6 shape types: lines, arrows, rectangles, circles, triangles, and freehand curves.",[752,1000,1001,1002,1005],{},"Flutter offers the ",[920,1003,1004],{},"CustomPainter"," class, which provides full control over drawing:",[929,1007,1011],{"className":1008,"code":1009,"language":1010,"meta":934,"style":934},"language-dart shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","class DrawingPainter extends CustomPainter {\n  final List\u003CShape> shapes;\n\n  @override\n  void paint(Canvas canvas, Size size) {\n    for (var shape in shapes) {\n      final paint = Paint()\n        ..color = shape.color\n        ..strokeWidth = shape.strokeWidth\n        ..style = PaintingStyle.stroke;\n\n      canvas.drawLine(shape.start, shape.end, paint);\n    }\n  }\n}\n","dart",[920,1012,1013,1031,1051,1058,1064,1092,1114,1132,1152,1169,1189,1194,1229,1235,1241],{"__ignoreMap":934},[938,1014,1015,1018,1022,1025,1028],{"class":940,"line":941},[938,1016,1017],{"class":944},"class",[938,1019,1021],{"class":1020},"sBMFI"," DrawingPainter",[938,1023,1024],{"class":944}," extends",[938,1026,1027],{"class":1020}," CustomPainter",[938,1029,1030],{"class":955}," {\n",[938,1032,1033,1037,1040,1042,1045,1048],{"class":940,"line":967},[938,1034,1036],{"class":1035},"spNyl","  final",[938,1038,1039],{"class":1020}," List",[938,1041,945],{"class":955},[938,1043,1044],{"class":1020},"Shape",[938,1046,1047],{"class":955},"> shapes",[938,1049,1050],{"class":944},";\n",[938,1052,1054],{"class":940,"line":1053},3,[938,1055,1057],{"emptyLinePlaceholder":1056},true,"\n",[938,1059,1061],{"class":940,"line":1060},4,[938,1062,1063],{"class":1035},"  @override\n",[938,1065,1067,1070,1074,1077,1080,1083,1086,1089],{"class":940,"line":1066},5,[938,1068,1069],{"class":1035},"  void",[938,1071,1073],{"class":1072},"s2Zo4"," paint",[938,1075,1076],{"class":955},"(",[938,1078,1079],{"class":1020},"Canvas",[938,1081,1082],{"class":955}," canvas",[938,1084,1085],{"class":944},",",[938,1087,1088],{"class":1020}," Size",[938,1090,1091],{"class":955}," size) {\n",[938,1093,1095,1099,1102,1105,1108,1111],{"class":940,"line":1094},6,[938,1096,1098],{"class":1097},"s7zQu","    for",[938,1100,1101],{"class":955}," (",[938,1103,1104],{"class":1035},"var",[938,1106,1107],{"class":955}," shape ",[938,1109,1110],{"class":1097},"in",[938,1112,1113],{"class":955}," shapes) {\n",[938,1115,1117,1120,1123,1126,1129],{"class":940,"line":1116},7,[938,1118,1119],{"class":1035},"      final",[938,1121,1122],{"class":955}," paint ",[938,1124,1125],{"class":944},"=",[938,1127,1128],{"class":1020}," Paint",[938,1130,1131],{"class":955},"()\n",[938,1133,1135,1138,1141,1143,1146,1149],{"class":940,"line":1134},8,[938,1136,1137],{"class":944},"        ..",[938,1139,1140],{"class":955},"color ",[938,1142,1125],{"class":944},[938,1144,1145],{"class":955}," shape",[938,1147,1148],{"class":944},".",[938,1150,1151],{"class":955},"color\n",[938,1153,1155,1157,1160,1162,1164,1166],{"class":940,"line":1154},9,[938,1156,1137],{"class":944},[938,1158,1159],{"class":955},"strokeWidth ",[938,1161,1125],{"class":944},[938,1163,1145],{"class":955},[938,1165,1148],{"class":944},[938,1167,1168],{"class":955},"strokeWidth\n",[938,1170,1172,1174,1177,1179,1182,1184,1187],{"class":940,"line":1171},10,[938,1173,1137],{"class":944},[938,1175,1176],{"class":955},"style ",[938,1178,1125],{"class":944},[938,1180,1181],{"class":1020}," PaintingStyle",[938,1183,1148],{"class":944},[938,1185,1186],{"class":955},"stroke",[938,1188,1050],{"class":944},[938,1190,1192],{"class":940,"line":1191},11,[938,1193,1057],{"emptyLinePlaceholder":1056},[938,1195,1197,1200,1202,1205,1208,1210,1213,1215,1217,1219,1222,1224,1227],{"class":940,"line":1196},12,[938,1198,1199],{"class":955},"      canvas",[938,1201,1148],{"class":944},[938,1203,1204],{"class":1072},"drawLine",[938,1206,1207],{"class":955},"(shape",[938,1209,1148],{"class":944},[938,1211,1212],{"class":955},"start",[938,1214,1085],{"class":944},[938,1216,1145],{"class":955},[938,1218,1148],{"class":944},[938,1220,1221],{"class":955},"end",[938,1223,1085],{"class":944},[938,1225,1226],{"class":955}," paint)",[938,1228,1050],{"class":944},[938,1230,1232],{"class":940,"line":1231},13,[938,1233,1234],{"class":955},"    }\n",[938,1236,1238],{"class":940,"line":1237},14,[938,1239,1240],{"class":955},"  }\n",[938,1242,1244],{"class":940,"line":1243},15,[938,1245,1246],{"class":955},"}\n",[752,1248,1249],{},"Coaches need different tools for different situations. An arrow shows the direction of movement, a circle highlights an error, a line compares body positions.",[752,1251,1252,1254],{},[755,1253,907],{}," Flutter offers a powerful drawing API, but it requires knowledge of geometry. It's an investment that pays off - a custom engine provides full control over functionality.",[774,1256,1258],{"id":1257},"_4-angle-measurement-mathematics-in-service-of-sport","4. Angle Measurement - Mathematics in Service of Sport",[752,1260,1261],{},"A key feature for swimming coaches - technique angle measurement - required implementing vector mathematics. We use the dot product of vectors and the law of cosines:",[929,1263,1265],{"className":1008,"code":1264,"language":1010,"meta":934,"style":934},"double calculateAngle(Offset p1, Offset vertex, Offset p2) {\n  final v1 = p1 - vertex;\n  final v2 = p2 - vertex;\n\n  final dotProduct = v1.dx * v2.dx + v1.dy * v2.dy;\n  final magnitude1 = sqrt(v1.dx * v1.dx + v1.dy * v1.dy);\n  final magnitude2 = sqrt(v2.dx * v2.dx + v2.dy * v2.dy);\n\n  final cosine = dotProduct / (magnitude1 * magnitude2);\n  return acos(cosine) * (180 / pi); // result in degrees\n}\n",[920,1266,1267,1298,1317,1335,1339,1387,1433,1477,1481,1505,1537],{"__ignoreMap":934},[938,1268,1269,1272,1275,1277,1280,1283,1285,1288,1291,1293,1295],{"class":940,"line":941},[938,1270,1271],{"class":1020},"double",[938,1273,1274],{"class":1072}," calculateAngle",[938,1276,1076],{"class":955},[938,1278,1279],{"class":1020},"Offset",[938,1281,1282],{"class":955}," p1",[938,1284,1085],{"class":944},[938,1286,1287],{"class":1020}," Offset",[938,1289,1290],{"class":955}," vertex",[938,1292,1085],{"class":944},[938,1294,1287],{"class":1020},[938,1296,1297],{"class":955}," p2) {\n",[938,1299,1300,1302,1305,1307,1310,1313,1315],{"class":940,"line":967},[938,1301,1036],{"class":1035},[938,1303,1304],{"class":955}," v1 ",[938,1306,1125],{"class":944},[938,1308,1309],{"class":955}," p1 ",[938,1311,1312],{"class":944},"-",[938,1314,1290],{"class":955},[938,1316,1050],{"class":944},[938,1318,1319,1321,1324,1326,1329,1331,1333],{"class":940,"line":1053},[938,1320,1036],{"class":1035},[938,1322,1323],{"class":955}," v2 ",[938,1325,1125],{"class":944},[938,1327,1328],{"class":955}," p2 ",[938,1330,1312],{"class":944},[938,1332,1290],{"class":955},[938,1334,1050],{"class":944},[938,1336,1337],{"class":940,"line":1060},[938,1338,1057],{"emptyLinePlaceholder":1056},[938,1340,1341,1343,1346,1348,1351,1353,1356,1359,1362,1364,1366,1369,1371,1373,1376,1378,1380,1382,1385],{"class":940,"line":1066},[938,1342,1036],{"class":1035},[938,1344,1345],{"class":955}," dotProduct ",[938,1347,1125],{"class":944},[938,1349,1350],{"class":955}," v1",[938,1352,1148],{"class":944},[938,1354,1355],{"class":955},"dx ",[938,1357,1358],{"class":944},"*",[938,1360,1361],{"class":955}," v2",[938,1363,1148],{"class":944},[938,1365,1355],{"class":955},[938,1367,1368],{"class":944},"+",[938,1370,1350],{"class":955},[938,1372,1148],{"class":944},[938,1374,1375],{"class":955},"dy ",[938,1377,1358],{"class":944},[938,1379,1361],{"class":955},[938,1381,1148],{"class":944},[938,1383,1384],{"class":955},"dy",[938,1386,1050],{"class":944},[938,1388,1389,1391,1394,1396,1399,1402,1404,1406,1408,1410,1412,1414,1416,1418,1420,1422,1424,1426,1428,1431],{"class":940,"line":1094},[938,1390,1036],{"class":1035},[938,1392,1393],{"class":955}," magnitude1 ",[938,1395,1125],{"class":944},[938,1397,1398],{"class":1072}," sqrt",[938,1400,1401],{"class":955},"(v1",[938,1403,1148],{"class":944},[938,1405,1355],{"class":955},[938,1407,1358],{"class":944},[938,1409,1350],{"class":955},[938,1411,1148],{"class":944},[938,1413,1355],{"class":955},[938,1415,1368],{"class":944},[938,1417,1350],{"class":955},[938,1419,1148],{"class":944},[938,1421,1375],{"class":955},[938,1423,1358],{"class":944},[938,1425,1350],{"class":955},[938,1427,1148],{"class":944},[938,1429,1430],{"class":955},"dy)",[938,1432,1050],{"class":944},[938,1434,1435,1437,1440,1442,1444,1447,1449,1451,1453,1455,1457,1459,1461,1463,1465,1467,1469,1471,1473,1475],{"class":940,"line":1116},[938,1436,1036],{"class":1035},[938,1438,1439],{"class":955}," magnitude2 ",[938,1441,1125],{"class":944},[938,1443,1398],{"class":1072},[938,1445,1446],{"class":955},"(v2",[938,1448,1148],{"class":944},[938,1450,1355],{"class":955},[938,1452,1358],{"class":944},[938,1454,1361],{"class":955},[938,1456,1148],{"class":944},[938,1458,1355],{"class":955},[938,1460,1368],{"class":944},[938,1462,1361],{"class":955},[938,1464,1148],{"class":944},[938,1466,1375],{"class":955},[938,1468,1358],{"class":944},[938,1470,1361],{"class":955},[938,1472,1148],{"class":944},[938,1474,1430],{"class":955},[938,1476,1050],{"class":944},[938,1478,1479],{"class":940,"line":1134},[938,1480,1057],{"emptyLinePlaceholder":1056},[938,1482,1483,1485,1488,1490,1492,1495,1498,1500,1503],{"class":940,"line":1154},[938,1484,1036],{"class":1035},[938,1486,1487],{"class":955}," cosine ",[938,1489,1125],{"class":944},[938,1491,1345],{"class":955},[938,1493,1494],{"class":944},"/",[938,1496,1497],{"class":955}," (magnitude1 ",[938,1499,1358],{"class":944},[938,1501,1502],{"class":955}," magnitude2)",[938,1504,1050],{"class":944},[938,1506,1507,1510,1513,1516,1518,1520,1524,1527,1530,1533],{"class":940,"line":1171},[938,1508,1509],{"class":1097},"  return",[938,1511,1512],{"class":1072}," acos",[938,1514,1515],{"class":955},"(cosine) ",[938,1517,1358],{"class":944},[938,1519,1101],{"class":955},[938,1521,1523],{"class":1522},"sbssI","180",[938,1525,1526],{"class":944}," /",[938,1528,1529],{"class":955}," pi)",[938,1531,1532],{"class":944},";",[938,1534,1536],{"class":1535},"sHwdD"," // result in degrees\n",[938,1538,1539],{"class":940,"line":1191},[938,1540,1246],{"class":955},[752,1542,1543],{},"Elbow bend angle, hand entry angle into the water, head position relative to torso - these are concrete numbers that a coach can show to an athlete. \"Bend your elbow 15 degrees more\" is more effective than \"bend more.\"",[752,1545,1546,1548],{},[755,1547,907],{}," Applications for a specific industry often require domain knowledge beyond typical programming.",[774,1550,1552],{"id":1551},"_5-interactive-objects-dragging-and-resizing","5. Interactive Objects - Dragging and Resizing",[752,1554,1555],{},"Implementing dragging and resizing of drawn shapes proved to be one of the more difficult challenges. It requires detecting whether a finger touches a shape, tracking the \"selected\" element, and smoothly updating its position.",[929,1557,1559],{"className":1008,"code":1558,"language":1010,"meta":934,"style":934},"bool isPointInsideShape(Shape shape, Offset point) {\n  final rect = Rect.fromPoints(shape.start, shape.end);\n  return rect.contains(point);\n}\n",[920,1560,1561,1582,1616,1633],{"__ignoreMap":934},[938,1562,1563,1566,1569,1571,1573,1575,1577,1579],{"class":940,"line":941},[938,1564,1565],{"class":1020},"bool",[938,1567,1568],{"class":1072}," isPointInsideShape",[938,1570,1076],{"class":955},[938,1572,1044],{"class":1020},[938,1574,1145],{"class":955},[938,1576,1085],{"class":944},[938,1578,1287],{"class":1020},[938,1580,1581],{"class":955}," point) {\n",[938,1583,1584,1586,1589,1591,1594,1596,1599,1601,1603,1605,1607,1609,1611,1614],{"class":940,"line":967},[938,1585,1036],{"class":1035},[938,1587,1588],{"class":955}," rect ",[938,1590,1125],{"class":944},[938,1592,1593],{"class":1020}," Rect",[938,1595,1148],{"class":944},[938,1597,1598],{"class":1072},"fromPoints",[938,1600,1207],{"class":955},[938,1602,1148],{"class":944},[938,1604,1212],{"class":955},[938,1606,1085],{"class":944},[938,1608,1145],{"class":955},[938,1610,1148],{"class":944},[938,1612,1613],{"class":955},"end)",[938,1615,1050],{"class":944},[938,1617,1618,1620,1623,1625,1628,1631],{"class":940,"line":1053},[938,1619,1509],{"class":1097},[938,1621,1622],{"class":955}," rect",[938,1624,1148],{"class":944},[938,1626,1627],{"class":1072},"contains",[938,1629,1630],{"class":955},"(point)",[938,1632,1050],{"class":944},[938,1634,1635],{"class":940,"line":1060},[938,1636,1246],{"class":955},[752,1638,1639],{},"A coach draws an arrow, but the athlete asks about a different moment - they need to quickly move the annotation without redrawing. Workflow fluidity is fundamental.",[752,1641,1642,1644],{},[755,1643,907],{}," Interactive graphic elements are much harder than static drawing, but essential for good UX.",[774,1646,1648],{"id":1647},"_6-spotlight-effect-focusing-viewer-attention","6. Spotlight Effect - Focusing Viewer Attention",[752,1650,1651],{},"Implementing the \"spotlight\" effect - dimming the background with a cut-out circle - required a multi-layered approach with two independent graphic components.",[752,1653,1654],{},"When a coach shows hand position, the rest of the frame is distracting. Spotlight allows focusing the athlete's attention exactly where needed. It's the difference between \"look here\" and actually looking.",[752,1656,1657,1659],{},[755,1658,907],{}," Visual effects require thoughtful layer architecture, but dramatically improve application usability.",[774,1661,1663],{"id":1662},"_7-screen-recording-platform-limitations","7. Screen Recording - Platform Limitations",[752,1665,1666],{},"Attempting to add screen recording revealed the painful truth about cross-platform development. The library used simply doesn't work the same on all systems.",[929,1668,1670],{"className":1008,"code":1669,"language":1010,"meta":934,"style":934},"if (!Platform.isMacOS) {\n  await FlutterScreenRecording.startRecordScreen(fileName);\n} else {\n  showMessage(\"Screen recording is not available on this platform\");\n}\n",[920,1671,1672,1690,1708,1718,1734],{"__ignoreMap":934},[938,1673,1674,1677,1679,1682,1685,1687],{"class":940,"line":941},[938,1675,1676],{"class":1097},"if",[938,1678,1101],{"class":955},[938,1680,1681],{"class":944},"!",[938,1683,1684],{"class":1020},"Platform",[938,1686,1148],{"class":944},[938,1688,1689],{"class":955},"isMacOS) {\n",[938,1691,1692,1695,1698,1700,1703,1706],{"class":940,"line":967},[938,1693,1694],{"class":1097},"  await",[938,1696,1697],{"class":1020}," FlutterScreenRecording",[938,1699,1148],{"class":944},[938,1701,1702],{"class":1072},"startRecordScreen",[938,1704,1705],{"class":955},"(fileName)",[938,1707,1050],{"class":944},[938,1709,1710,1713,1716],{"class":940,"line":1053},[938,1711,1712],{"class":955},"} ",[938,1714,1715],{"class":1097},"else",[938,1717,1030],{"class":955},[938,1719,1720,1723,1725,1729,1732],{"class":940,"line":1060},[938,1721,1722],{"class":1072},"  showMessage",[938,1724,1076],{"class":955},[938,1726,1728],{"class":1727},"sfazB","\"Screen recording is not available on this platform\"",[938,1730,1731],{"class":955},")",[938,1733,1050],{"class":944},[938,1735,1736],{"class":940,"line":1066},[938,1737,1246],{"class":955},[752,1739,1740],{},"A coach wants to record their analysis with voice commentary and send it to an athlete. This is a key feature for remote work with athletes.",[752,1742,1743,1745],{},[755,1744,907],{}," \"Cross-platform\" doesn't mean \"works identically everywhere.\" You need to test on all target devices and sometimes accept limitations.",[774,1747,1749],{"id":1748},"_8-user-interface-dozens-of-iterations","8. User Interface - Dozens of Iterations",[752,1751,1752],{},"Finding the optimal control layout required many iterations. We started with controls in the right sidebar, then moved them to the bottom, added a play icon over the video, removed the sidebar, centered the toolbar...",[752,1754,1755],{},"A coach often holds the tablet with one hand while pointing something out to an athlete with the other. Controls must be accessible with a thumb, cannot obscure the video, and must be readable at the pool in full sunlight.",[752,1757,1758,1760],{},[755,1759,907],{}," UX requires experimentation and feedback collection. You cannot design the perfect interface on the first try.",[774,1762,1764],{"id":1763},"_9-responsiveness-iphone-vs-ipad","9. Responsiveness - iPhone vs iPad",[752,1766,1767],{},"The app must work equally well on iPhone (compact screen, on-the-go analysis) and iPad (larger screen, detailed work). Drawing controls require different proportions on each device.",[929,1769,1771],{"className":1008,"code":1770,"language":1010,"meta":934,"style":934},"final isTablet = MediaQuery.of(context).size.shortestSide >= 600;\nfinal iconSize = isTablet ? 32.0 : 24.0;\n",[920,1772,1773,1812],{"__ignoreMap":934},[938,1774,1775,1778,1781,1783,1786,1788,1791,1794,1796,1799,1801,1804,1807,1810],{"class":940,"line":941},[938,1776,1777],{"class":1035},"final",[938,1779,1780],{"class":955}," isTablet ",[938,1782,1125],{"class":944},[938,1784,1785],{"class":1020}," MediaQuery",[938,1787,1148],{"class":944},[938,1789,1790],{"class":1072},"of",[938,1792,1793],{"class":955},"(context)",[938,1795,1148],{"class":944},[938,1797,1798],{"class":955},"size",[938,1800,1148],{"class":944},[938,1802,1803],{"class":955},"shortestSide ",[938,1805,1806],{"class":944},">=",[938,1808,1809],{"class":1522}," 600",[938,1811,1050],{"class":944},[938,1813,1814,1816,1819,1821,1823,1826,1829,1832,1835],{"class":940,"line":967},[938,1815,1777],{"class":1035},[938,1817,1818],{"class":955}," iconSize ",[938,1820,1125],{"class":944},[938,1822,1780],{"class":955},[938,1824,1825],{"class":944},"?",[938,1827,1828],{"class":1522}," 32.0",[938,1830,1831],{"class":944}," :",[938,1833,1834],{"class":1522}," 24.0",[938,1836,1050],{"class":944},[752,1838,1839],{},"A coach uses the iPad during training at the pool, but the athlete watches the analysis on their iPhone at home. The experience must be consistent.",[752,1841,1842,1844],{},[755,1843,907],{}," Responsive design requires conscious design for each form factor, not just interface scaling.",[774,1846,1848],{"id":1847},"_10-system-integration-sharing-and-export","10. System Integration - Sharing and Export",[752,1850,1851],{},"Every \"simple\" system integration - opening links, sharing, saving to gallery - turned out to require more work than we expected.",[929,1853,1855],{"className":1008,"code":1854,"language":1010,"meta":934,"style":934},"await Share.shareFiles([screenshotPath], text: 'Technique analysis');\n",[920,1856,1857],{"__ignoreMap":934},[938,1858,1859,1862,1865,1867,1870,1873,1875,1878,1881,1884,1886],{"class":940,"line":941},[938,1860,1861],{"class":1097},"await",[938,1863,1864],{"class":1020}," Share",[938,1866,1148],{"class":944},[938,1868,1869],{"class":1072},"shareFiles",[938,1871,1872],{"class":955},"([screenshotPath]",[938,1874,1085],{"class":944},[938,1876,1877],{"class":955}," text",[938,1879,1880],{"class":944},":",[938,1882,1883],{"class":1727}," 'Technique analysis'",[938,1885,1731],{"class":955},[938,1887,1050],{"class":944},[752,1889,1890],{},"A coach takes a screenshot with annotations and wants to send it via WhatsApp. If that requires 5 steps instead of 2, they simply won't do it.",[752,1892,1893,1895],{},[755,1894,907],{}," System integrations are the \"last mile\" - often underestimated, but crucial for real-world app usage.",[767,1897],{},[747,1899,1901],{"id":1900},"summary","Summary",[752,1903,1904],{},"The development history of BeatBuddy Replay shows a typical product-building path:",[1906,1907,1908,1921],"table",{},[1909,1910,1911],"thead",{},[1912,1913,1914,1918],"tr",{},[1915,1916,1917],"th",{},"Phase",[1915,1919,1920],{},"Focus",[1922,1923,1924,1933,1941,1949,1957],"tbody",{},[1912,1925,1926,1930],{},[1927,1928,1929],"td",{},"Quick start",[1927,1931,1932],{},"Working app in one day",[1912,1934,1935,1938],{},[1927,1936,1937],{},"Platform issues",[1927,1939,1940],{},"Crashes, debugging, iOS adaptation",[1912,1942,1943,1946],{},[1927,1944,1945],{},"Feature expansion",[1927,1947,1948],{},"Spotlight, object movement, protractor",[1912,1950,1951,1954],{},[1927,1952,1953],{},"UX refinement",[1927,1955,1956],{},"Dozens of interface iterations",[1912,1958,1959,1962],{},[1927,1960,1961],{},"Stabilization",[1927,1963,1964],{},"Screen recording, sharing",[767,1966],{},[747,1968,1970],{"id":1969},"whats-next","What's Next?",[752,1972,1973],{},"We are working on:",[779,1975,1976,1982,1988],{},[782,1977,1978,1981],{},[755,1979,1980],{},"Android version"," - will expand app availability",[782,1983,1984,1987],{},[755,1985,1986],{},"Two-video comparison"," - side-by-side analysis, crucial for the Total Immersion method",[782,1989,1990,1993],{},[755,1991,1992],{},"Video library"," - collection of links to the best exercises on YouTube and recordings of world-class swimmers' technique, which will serve as reference models for analysis",[767,1995],{},[747,1997,1999],{"id":1998},"key-takeaways-for-other-developers","Key Takeaways for Other Developers",[752,2001,2002,2005],{},[755,2003,2004],{},"Speed over perfection"," - it's better to release a working product and iterate than to wait for the ideal.",[752,2007,2008,2011],{},[755,2009,2010],{},"Test on real devices"," - a simulator cannot replace an iPad in a coach's hands by the pool.",[752,2013,2014,2017],{},[755,2015,2016],{},"Listen to users"," - every UI iteration resulted from real feedback.",[752,2019,2020,2023],{},[755,2021,2022],{},"Cross-platform has its limits"," - accept limitations and communicate them clearly.",[752,2025,2026],{},"The app delivers a fully functional video analysis tool in just over 3,000 lines of code - proof that Flutter allows you to quickly build useful products for a specific niche.",[767,2028],{},[752,2030,2031],{},[2032,2033,2034,2035],"em",{},"BeatBuddy Replay is part of the BeatBuddy Platform ecosystem, which also includes BeatBuddy Pro - a programmable metronome device for swimmers. More information at ",[2036,2037,2041],"a",{"href":2038,"rel":2039},"https://beatbuddypro.com",[2040],"nofollow","beatbuddypro.com",[2043,2044,2045],"style",{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}",{"title":934,"searchDepth":967,"depth":967,"links":2047},[2048,2049,2055,2056,2068,2069,2070],{"id":749,"depth":967,"text":750},{"id":771,"depth":967,"text":772,"children":2050},[2051,2052,2053,2054],{"id":776,"depth":1053,"text":777},{"id":796,"depth":1053,"text":797},{"id":823,"depth":1053,"text":824},{"id":835,"depth":1053,"text":836},{"id":852,"depth":967,"text":853},{"id":888,"depth":967,"text":889,"children":2057},[2058,2059,2060,2061,2062,2063,2064,2065,2066,2067],{"id":895,"depth":1053,"text":896},{"id":911,"depth":1053,"text":912},{"id":994,"depth":1053,"text":995},{"id":1257,"depth":1053,"text":1258},{"id":1551,"depth":1053,"text":1552},{"id":1647,"depth":1053,"text":1648},{"id":1662,"depth":1053,"text":1663},{"id":1748,"depth":1053,"text":1749},{"id":1763,"depth":1053,"text":1764},{"id":1847,"depth":1053,"text":1848},{"id":1900,"depth":967,"text":1901},{"id":1969,"depth":967,"text":1970},{"id":1998,"depth":967,"text":1999},"sportstech","2026-01-17T00:00:00.000Z","A deep-dive into building a Flutter video analysis app for swimmers. From MVP architecture to cross-platform challenges - lessons from two weeks of development.","md",{"src":2076},"/images/blog/musictechlab_blog_beatbuddy-replay-video-analysis-app-for-swimmers.webp",{"enabled":1056,"items":2078},[2079,2082,2085,2088],{"text":2080,"icon":2081},"Flutter MVP was built in 2 weeks with 3,300 lines of code in a single file.","i-lucide-zap",{"text":2083,"icon":2084},"Cross-platform plugins often require hidden platform-specific iOS/Android configuration.","i-lucide-smartphone",{"text":2086,"icon":2087},"Vector math powers angle measurement, giving coaches precise technique feedback in degrees.","i-lucide-triangle",{"text":2089,"icon":2090},"Dozens of UI iterations were needed because coaches use tablets one-handed at the pool.","i-lucide-tablet",{},{"title":727,"description":2073},[2071,2094],"development","HN40SuTtPMzORZCJ7GzVwCBNWWL9c_yJcHijErrcNsU",[2097,2099],{"title":718,"path":719,"stem":720,"description":2098,"children":-1},"Sanity.io has a number of advantages over its alternatives, such as GraphCMS, Storyblok, Contentful, etc. Find out what they are...",{"title":731,"path":732,"stem":733,"description":2100,"children":-1},"Learn how to build a custom Garmin watch face using the Connect IQ SDK and Monkey C. A step-by-step tutorial based on our BeatBuddy watch face experiment.",[2102,4423,11611,13505],{"id":2103,"title":112,"authors":2104,"badge":2107,"body":2110,"category":4398,"client":742,"date":4399,"description":4400,"extension":2074,"faq":742,"featured":69,"featuredOrder":742,"hidden":69,"image":4401,"keyTakeaways":4403,"meta":4417,"navigation":1056,"path":113,"seo":4418,"status":742,"stem":114,"tags":4419,"teaser":742,"__hash__":4422,"score":941},"posts/blog/music-data/building-a-custom-music-delivery-platform-on-the-revelator-api.md",[2105],{"name":738,"to":739,"avatar":2106},{"src":741},{"label":2108,"color":2109},"Distribution","#0ea5e9",{"type":744,"value":2111,"toc":4366},[2112,2119,2121,2125,2128,2182,2185,2187,2191,2194,2203,2212,2219,2221,2225,2228,2231,2235,2252,2255,2284,2288,2299,2303,2314,2318,2329,2339,2341,2345,2348,2476,2480,2544,2548,2600,2604,2607,3360,3373,3375,3379,3382,3385,3389,3392,3615,3619,3622,3800,3804,3811,4007,4030,4034,4040,4058,4065,4067,4071,4074,4082,4086,4089,4093,4096,4151,4154,4158,4161,4165,4168,4170,4174,4177,4197,4200,4254,4263,4265,4269,4272,4298,4307,4309,4313,4316,4319,4321,4325,4363],[752,2113,2114,2115,2118],{},"Music distribution has never been more accessible. Platforms like DistroKid, TuneCore, and CD Baby democratized access to DSPs (Digital Service Providers) for independent artists. But for companies operating as distributors - managing ",[2036,2116,2117],{"href":77},"multiple labels, complex territory restrictions",", and custom royalty splits - the generic UI and rigid workflows of consumer-facing tools quickly become a bottleneck.",[767,2120],{},[747,2122,2124],{"id":2123},"why-distributors-outgrow-off-the-shelf-platforms","Why Distributors Outgrow Off-the-Shelf Platforms",[752,2126,2127],{},"The friction points are predictable:",[1906,2129,2130,2140],{},[1909,2131,2132],{},[1912,2133,2134,2137],{},[1915,2135,2136],{},"Pain Point",[1915,2138,2139],{},"What Happens",[1922,2141,2142,2152,2162,2172],{},[1912,2143,2144,2149],{},[1927,2145,2146],{},[755,2147,2148],{},"Territory restrictions",[1927,2150,2151],{},"Many platforms only support album-level territory settings, forcing workarounds when individual tracks have different rights across markets",[1912,2153,2154,2159],{},[1927,2155,2156],{},[755,2157,2158],{},"Metadata flexibility",[1927,2160,2161],{},"Contributor roles, localized titles, and DSP-specific artist IDs often require manual overrides the UI doesn't expose",[1912,2163,2164,2169],{},[1927,2165,2166],{},[755,2167,2168],{},"Reporting",[1927,2170,2171],{},"Off-the-shelf dashboards rarely match the operational needs of a multi-label distributor",[1912,2173,2174,2179],{},[1927,2175,2176],{},[755,2177,2178],{},"Delivery control",[1927,2180,2181],{},"When a platform handles delivery as a black box, debugging ingestion failures becomes a support ticket game",[752,2183,2184],{},"This is where the build-vs-buy decision becomes real.",[767,2186],{},[747,2188,2190],{"id":2189},"the-two-paths-frontend-only-vs-full-stack","The Two Paths: Frontend-Only vs. Full Stack",[752,2192,2193],{},"Distributors who've outgrown their current setup face a fundamental architectural choice:",[2195,2196,2197],"note",{},[752,2198,2199,2202],{},[755,2200,2201],{},"Option A: Custom Frontend + Revelator API as Backend","\nBuild your own user-facing layer - UX, workflows, internal tools, reporting - and use Revelator's API purely for catalog management, delivery to DSPs, and royalty ingestion.",[2204,2205,2206],"warning",{},[752,2207,2208,2211],{},[755,2209,2210],{},"Option B: Full-Stack Distribution Platform","\nReplace the platform entirely. Build metadata management, DDEX (Digital Data Exchange) generation, delivery pipelines, DSP integrations, royalty ingestion, reporting, and payouts from scratch.",[752,2213,2214,2215,2218],{},"This article focuses on ",[755,2216,2217],{},"Option A",": what it looks like in practice, where it excels, and where it hits structural limits.",[767,2220],{},[747,2222,2224],{"id":2223},"what-the-revelator-api-actually-offers","What the Revelator API Actually Offers",[752,2226,2227],{},"Revelator positions itself as an \"end-to-end operating system\" for independent music businesses. Unlike consumer-facing distributors, it's B2B infrastructure - designed for labels, aggregators, and distributors who want to run their own branded operation.",[752,2229,2230],{},"The API is RESTful, JSON-based, and covers five core modules:",[774,2232,2234],{"id":2233},"catalog-management","Catalog Management",[779,2236,2237,2243,2246,2249],{},[782,2238,2239,2240],{},"Create and edit releases via ",[920,2241,2242],{},"POST /content/release/save",[782,2244,2245],{},"Upload audio (WAV/FLAC, minimum 16-bit, 44.1 kHz stereo) and cover art (minimum 1400x1400px JPG/RGB)",[782,2247,2248],{},"Manage ISRCs (International Standard Recording Codes), UPCs (Universal Product Codes), and external DSP artist IDs",[782,2250,2251],{},"Support for multi-disc releases and localized metadata",[774,2253,2108],{"id":2254},"distribution",[779,2256,2257,2263,2266,2272,2275,2281],{},[782,2258,2259,2260],{},"Pre-delivery validation: ",[920,2261,2262],{},"POST /distribution/release/{releaseId}/validate",[782,2264,2265],{},"Set DSP targets, release dates, and territories per release",[782,2267,2268,2269],{},"Queue releases for delivery: ",[920,2270,2271],{},"POST /distribution/release/addtoqueue",[782,2273,2274],{},"Track delivery status (statuses range from -20 to 100; 50+ means delivered)",[782,2276,2277,2278],{},"Takedown support: ",[920,2279,2280],{},"POST /distribution/release/takedown",[782,2282,2283],{},"Webhook callbacks for delivery status updates",[774,2285,2287],{"id":2286},"rights-and-royalties","Rights and Royalties",[779,2289,2290,2293,2296],{},[782,2291,2292],{},"Contract definition with configurable splits and recoupables",[782,2294,2295],{},"DSP statement import and reconciliation",[782,2297,2298],{},"Multi-currency payout automation via Tipalti and PayPal",[774,2300,2302],{"id":2301},"analytics","Analytics",[779,2304,2305,2308,2311],{},[782,2306,2307],{},"Streaming and revenue data queryable by track, release, region, or DSP",[782,2309,2310],{},"Playlist performance tracking",[782,2312,2313],{},"CSV export and raw data sync",[774,2315,2317],{"id":2316},"account-management","Account Management",[779,2319,2320,2323,2326],{},[782,2321,2322],{},"Parent-child account hierarchy (your enterprise is the parent; each label or artist is a child)",[782,2324,2325],{},"Full visibility into child account assets",[782,2327,2328],{},"Mandatory approval step before content reaches DSPs",[2330,2331,2332],"tip",{},[752,2333,2334,2335,2338],{},"Revelator claims ",[755,2336,2337],{},"100+ DSP integrations",", including Spotify, Apple Music, Amazon, YouTube Music, YouTube Content ID, TikTok, and Deezer. DSP access is configurable per child account.",[767,2340],{},[747,2342,2344],{"id":2343},"architecture-of-the-hybrid-solution","Architecture of the Hybrid Solution",[752,2346,2347],{},"The hybrid approach layers your custom platform on top of Revelator's delivery infrastructure:",[929,2349,2353],{"className":2350,"code":2351,"language":2352,"meta":934,"style":934},"language-mermaid shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","graph TD\n    subgraph Frontend[Your Custom Frontend]\n        AP[Artist Portal]\n        LD[Label Dashboard]\n        AT[Admin Tools]\n        AP & LD & AT --> BE[Your API / Backend]\n    end\n    subgraph Revelator[Revelator API Layer]\n        CAT[Catalog Mgmt API]\n        DEL[Delivery API]\n        ROY[Royalty Ingestion]\n    end\n    BE --> CAT & DEL & ROY\n    subgraph DSPs[Digital Service Providers]\n        SP[Spotify]\n        AM[Apple Music]\n        YT[YouTube]\n        MORE[100+ more]\n    end\n    DEL --> SP & AM & YT & MORE\n    style Frontend fill:#0f172a,stroke:#38bdf8,color:#f8fafc\n    style Revelator fill:#1e293b,stroke:#0ea5e9,color:#f8fafc\n    style DSPs fill:#0f172a,stroke:#334155,color:#f8fafc\n","mermaid",[920,2354,2355,2360,2365,2370,2375,2380,2385,2390,2395,2400,2405,2410,2414,2419,2424,2429,2435,2441,2447,2452,2458,2464,2470],{"__ignoreMap":934},[938,2356,2357],{"class":940,"line":941},[938,2358,2359],{"class":955},"graph TD\n",[938,2361,2362],{"class":940,"line":967},[938,2363,2364],{"class":955},"    subgraph Frontend[Your Custom Frontend]\n",[938,2366,2367],{"class":940,"line":1053},[938,2368,2369],{"class":955},"        AP[Artist Portal]\n",[938,2371,2372],{"class":940,"line":1060},[938,2373,2374],{"class":955},"        LD[Label Dashboard]\n",[938,2376,2377],{"class":940,"line":1066},[938,2378,2379],{"class":955},"        AT[Admin Tools]\n",[938,2381,2382],{"class":940,"line":1094},[938,2383,2384],{"class":955},"        AP & LD & AT --> BE[Your API / Backend]\n",[938,2386,2387],{"class":940,"line":1116},[938,2388,2389],{"class":955},"    end\n",[938,2391,2392],{"class":940,"line":1134},[938,2393,2394],{"class":955},"    subgraph Revelator[Revelator API Layer]\n",[938,2396,2397],{"class":940,"line":1154},[938,2398,2399],{"class":955},"        CAT[Catalog Mgmt API]\n",[938,2401,2402],{"class":940,"line":1171},[938,2403,2404],{"class":955},"        DEL[Delivery API]\n",[938,2406,2407],{"class":940,"line":1191},[938,2408,2409],{"class":955},"        ROY[Royalty Ingestion]\n",[938,2411,2412],{"class":940,"line":1196},[938,2413,2389],{"class":955},[938,2415,2416],{"class":940,"line":1231},[938,2417,2418],{"class":955},"    BE --> CAT & DEL & ROY\n",[938,2420,2421],{"class":940,"line":1237},[938,2422,2423],{"class":955},"    subgraph DSPs[Digital Service Providers]\n",[938,2425,2426],{"class":940,"line":1243},[938,2427,2428],{"class":955},"        SP[Spotify]\n",[938,2430,2432],{"class":940,"line":2431},16,[938,2433,2434],{"class":955},"        AM[Apple Music]\n",[938,2436,2438],{"class":940,"line":2437},17,[938,2439,2440],{"class":955},"        YT[YouTube]\n",[938,2442,2444],{"class":940,"line":2443},18,[938,2445,2446],{"class":955},"        MORE[100+ more]\n",[938,2448,2450],{"class":940,"line":2449},19,[938,2451,2389],{"class":955},[938,2453,2455],{"class":940,"line":2454},20,[938,2456,2457],{"class":955},"    DEL --> SP & AM & YT & MORE\n",[938,2459,2461],{"class":940,"line":2460},21,[938,2462,2463],{"class":955},"    style Frontend fill:#0f172a,stroke:#38bdf8,color:#f8fafc\n",[938,2465,2467],{"class":940,"line":2466},22,[938,2468,2469],{"class":955},"    style Revelator fill:#1e293b,stroke:#0ea5e9,color:#f8fafc\n",[938,2471,2473],{"class":940,"line":2472},23,[938,2474,2475],{"class":955},"    style DSPs fill:#0f172a,stroke:#334155,color:#f8fafc\n",[774,2477,2479],{"id":2478},"what-you-build","What You Build",[1906,2481,2482,2492],{},[1909,2483,2484],{},[1912,2485,2486,2489],{},[1915,2487,2488],{},"Layer",[1915,2490,2491],{},"Responsibility",[1922,2493,2494,2504,2514,2524,2534],{},[1912,2495,2496,2501],{},[1927,2497,2498],{},[755,2499,2500],{},"Artist & label portals",[1927,2502,2503],{},"Branded onboarding, release submission, approval workflows",[1912,2505,2506,2511],{},[1927,2507,2508],{},[755,2509,2510],{},"Metadata layer",[1927,2512,2513],{},"Your own database of releases, tracks, contributors, and rights - synced to Revelator via API",[1912,2515,2516,2521],{},[1927,2517,2518],{},[755,2519,2520],{},"Territory & rights engine",[1927,2522,2523],{},"Business logic for track-level territory restrictions, split sheets, and embargo rules",[1912,2525,2526,2531],{},[1927,2527,2528],{},[755,2529,2530],{},"Reporting dashboards",[1927,2532,2533],{},"Custom views pulling from Revelator's analytics API plus your own data",[1912,2535,2536,2541],{},[1927,2537,2538],{},[755,2539,2540],{},"Internal tools",[1927,2542,2543],{},"Approval queues, QC checklists, bulk operations, CRM integration",[774,2545,2547],{"id":2546},"what-revelator-handles","What Revelator Handles",[1906,2549,2550,2558],{},[1909,2551,2552],{},[1912,2553,2554,2556],{},[1915,2555,2488],{},[1915,2557,2491],{},[1922,2559,2560,2570,2580,2590],{},[1912,2561,2562,2567],{},[1927,2563,2564],{},[755,2565,2566],{},"DDEX generation",[1927,2568,2569],{},"Generates ERN (Electronic Release Notification) XML packages from catalog data you push via API",[1912,2571,2572,2577],{},[1927,2573,2574],{},[755,2575,2576],{},"DSP delivery",[1927,2578,2579],{},"SFTP/API delivery to all connected DSPs, including retry logic and status tracking",[1912,2581,2582,2587],{},[1927,2583,2584],{},[755,2585,2586],{},"Royalty ingestion",[1927,2588,2589],{},"Parsing DSP statements and normalizing revenue data",[1912,2591,2592,2597],{},[1927,2593,2594],{},[755,2595,2596],{},"Payout infrastructure",[1927,2598,2599],{},"Payment rail integrations for artist payouts",[774,2601,2603],{"id":2602},"quick-example-creating-a-release-via-revelator-api","Quick Example: Creating a Release via Revelator API",[752,2605,2606],{},"Here's a minimal example of how your backend would create and distribute a release through Revelator's API:",[929,2608,2612],{"className":2609,"code":2610,"language":2611,"meta":934,"style":934},"language-python shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import httpx\n\nREVELATOR_API = \"https://api.revelator.com\"\n\n# 1. Authenticate\nauth = httpx.post(f\"{REVELATOR_API}/partner/account/login\", json={\n    \"email\": \"your@email.com\",\n    \"password\": \"your-password\"\n})\ntoken = auth.json()[\"token\"]\nheaders = {\"Authorization\": f\"Bearer {token}\"}\n\n# 2. Create a release\nrelease = httpx.post(f\"{REVELATOR_API}/content/release/save\", headers=headers, json={\n    \"title\": \"Delilah - Summer Version\",\n    \"releaseType\": \"Single\",\n    \"artists\": [\n        {\"name\": \"MIKOLAS\", \"role\": \"MainArtist\"},\n        {\"name\": \"Mark Neve\", \"role\": \"MainArtist\"}\n    ],\n    \"genre\": \"Pop\",\n    \"releaseDate\": \"2026-04-01\",\n    \"territories\": {\n        \"include\": \"Worldwide\",\n        \"exclude\": [\"JP\", \"DE\", \"AT\", \"CH\", \"NL\", \"AU\", \"NZ\"]\n    }\n})\nrelease_id = release.json()[\"id\"]\n\n# 3. Validate before delivery\nvalidation = httpx.post(\n    f\"{REVELATOR_API}/distribution/release/{release_id}/validate\",\n    headers=headers\n)\n\n# 4. Queue for delivery to DSPs\nif validation.json()[\"valid\"]:\n    httpx.post(f\"{REVELATOR_API}/distribution/release/addtoqueue\",\n        headers=headers,\n        json={\"releaseId\": release_id}\n    )\n","python",[920,2613,2614,2622,2626,2642,2646,2651,2695,2717,2735,2740,2768,2803,2807,2812,2856,2876,2896,2910,2952,2989,2994,3014,3034,3047,3069,3147,3152,3157,3183,3188,3194,3211,3240,3251,3257,3262,3268,3292,3319,3331,3354],{"__ignoreMap":934},[938,2615,2616,2619],{"class":940,"line":941},[938,2617,2618],{"class":1097},"import",[938,2620,2621],{"class":955}," httpx\n",[938,2623,2624],{"class":940,"line":967},[938,2625,1057],{"emptyLinePlaceholder":1056},[938,2627,2628,2631,2633,2636,2639],{"class":940,"line":1053},[938,2629,2630],{"class":955},"REVELATOR_API ",[938,2632,1125],{"class":944},[938,2634,2635],{"class":944}," \"",[938,2637,2638],{"class":1727},"https://api.revelator.com",[938,2640,2641],{"class":944},"\"\n",[938,2643,2644],{"class":940,"line":1060},[938,2645,1057],{"emptyLinePlaceholder":1056},[938,2647,2648],{"class":940,"line":1066},[938,2649,2650],{"class":1535},"# 1. Authenticate\n",[938,2652,2653,2656,2658,2661,2663,2666,2668,2671,2674,2677,2680,2683,2686,2688,2692],{"class":940,"line":1094},[938,2654,2655],{"class":955},"auth ",[938,2657,1125],{"class":944},[938,2659,2660],{"class":955}," httpx",[938,2662,1148],{"class":944},[938,2664,2665],{"class":1072},"post",[938,2667,1076],{"class":944},[938,2669,2670],{"class":1035},"f",[938,2672,2673],{"class":1727},"\"",[938,2675,2676],{"class":1522},"{",[938,2678,2679],{"class":1072},"REVELATOR_API",[938,2681,2682],{"class":1522},"}",[938,2684,2685],{"class":1727},"/partner/account/login\"",[938,2687,1085],{"class":944},[938,2689,2691],{"class":2690},"sHdIc"," json",[938,2693,2694],{"class":944},"={\n",[938,2696,2697,2700,2703,2705,2707,2709,2712,2714],{"class":940,"line":1116},[938,2698,2699],{"class":944},"    \"",[938,2701,2702],{"class":1727},"email",[938,2704,2673],{"class":944},[938,2706,1880],{"class":944},[938,2708,2635],{"class":944},[938,2710,2711],{"class":1727},"your@email.com",[938,2713,2673],{"class":944},[938,2715,2716],{"class":944},",\n",[938,2718,2719,2721,2724,2726,2728,2730,2733],{"class":940,"line":1134},[938,2720,2699],{"class":944},[938,2722,2723],{"class":1727},"password",[938,2725,2673],{"class":944},[938,2727,1880],{"class":944},[938,2729,2635],{"class":944},[938,2731,2732],{"class":1727},"your-password",[938,2734,2641],{"class":944},[938,2736,2737],{"class":940,"line":1154},[938,2738,2739],{"class":944},"})\n",[938,2741,2742,2745,2747,2750,2752,2755,2758,2760,2763,2765],{"class":940,"line":1171},[938,2743,2744],{"class":955},"token ",[938,2746,1125],{"class":944},[938,2748,2749],{"class":955}," auth",[938,2751,1148],{"class":944},[938,2753,2754],{"class":1072},"json",[938,2756,2757],{"class":944},"()[",[938,2759,2673],{"class":944},[938,2761,2762],{"class":1727},"token",[938,2764,2673],{"class":944},[938,2766,2767],{"class":944},"]\n",[938,2769,2770,2773,2775,2778,2780,2783,2785,2787,2790,2793,2795,2797,2799,2801],{"class":940,"line":1191},[938,2771,2772],{"class":955},"headers ",[938,2774,1125],{"class":944},[938,2776,2777],{"class":944}," {",[938,2779,2673],{"class":944},[938,2781,2782],{"class":1727},"Authorization",[938,2784,2673],{"class":944},[938,2786,1880],{"class":944},[938,2788,2789],{"class":1035}," f",[938,2791,2792],{"class":1727},"\"Bearer ",[938,2794,2676],{"class":1522},[938,2796,2762],{"class":955},[938,2798,2682],{"class":1522},[938,2800,2673],{"class":1727},[938,2802,1246],{"class":944},[938,2804,2805],{"class":940,"line":1196},[938,2806,1057],{"emptyLinePlaceholder":1056},[938,2808,2809],{"class":940,"line":1231},[938,2810,2811],{"class":1535},"# 2. Create a release\n",[938,2813,2814,2817,2819,2821,2823,2825,2827,2829,2831,2833,2835,2837,2840,2842,2845,2847,2850,2852,2854],{"class":940,"line":1237},[938,2815,2816],{"class":955},"release ",[938,2818,1125],{"class":944},[938,2820,2660],{"class":955},[938,2822,1148],{"class":944},[938,2824,2665],{"class":1072},[938,2826,1076],{"class":944},[938,2828,2670],{"class":1035},[938,2830,2673],{"class":1727},[938,2832,2676],{"class":1522},[938,2834,2679],{"class":1072},[938,2836,2682],{"class":1522},[938,2838,2839],{"class":1727},"/content/release/save\"",[938,2841,1085],{"class":944},[938,2843,2844],{"class":2690}," headers",[938,2846,1125],{"class":944},[938,2848,2849],{"class":1072},"headers",[938,2851,1085],{"class":944},[938,2853,2691],{"class":2690},[938,2855,2694],{"class":944},[938,2857,2858,2860,2863,2865,2867,2869,2872,2874],{"class":940,"line":1243},[938,2859,2699],{"class":944},[938,2861,2862],{"class":1727},"title",[938,2864,2673],{"class":944},[938,2866,1880],{"class":944},[938,2868,2635],{"class":944},[938,2870,2871],{"class":1727},"Delilah - Summer Version",[938,2873,2673],{"class":944},[938,2875,2716],{"class":944},[938,2877,2878,2880,2883,2885,2887,2889,2892,2894],{"class":940,"line":2431},[938,2879,2699],{"class":944},[938,2881,2882],{"class":1727},"releaseType",[938,2884,2673],{"class":944},[938,2886,1880],{"class":944},[938,2888,2635],{"class":944},[938,2890,2891],{"class":1727},"Single",[938,2893,2673],{"class":944},[938,2895,2716],{"class":944},[938,2897,2898,2900,2903,2905,2907],{"class":940,"line":2437},[938,2899,2699],{"class":944},[938,2901,2902],{"class":1727},"artists",[938,2904,2673],{"class":944},[938,2906,1880],{"class":944},[938,2908,2909],{"class":944}," [\n",[938,2911,2912,2915,2917,2920,2922,2924,2926,2929,2931,2933,2935,2938,2940,2942,2944,2947,2949],{"class":940,"line":2443},[938,2913,2914],{"class":944},"        {",[938,2916,2673],{"class":944},[938,2918,2919],{"class":1727},"name",[938,2921,2673],{"class":944},[938,2923,1880],{"class":944},[938,2925,2635],{"class":944},[938,2927,2928],{"class":1727},"MIKOLAS",[938,2930,2673],{"class":944},[938,2932,1085],{"class":944},[938,2934,2635],{"class":944},[938,2936,2937],{"class":1727},"role",[938,2939,2673],{"class":944},[938,2941,1880],{"class":944},[938,2943,2635],{"class":944},[938,2945,2946],{"class":1727},"MainArtist",[938,2948,2673],{"class":944},[938,2950,2951],{"class":944},"},\n",[938,2953,2954,2956,2958,2960,2962,2964,2966,2969,2971,2973,2975,2977,2979,2981,2983,2985,2987],{"class":940,"line":2449},[938,2955,2914],{"class":944},[938,2957,2673],{"class":944},[938,2959,2919],{"class":1727},[938,2961,2673],{"class":944},[938,2963,1880],{"class":944},[938,2965,2635],{"class":944},[938,2967,2968],{"class":1727},"Mark Neve",[938,2970,2673],{"class":944},[938,2972,1085],{"class":944},[938,2974,2635],{"class":944},[938,2976,2937],{"class":1727},[938,2978,2673],{"class":944},[938,2980,1880],{"class":944},[938,2982,2635],{"class":944},[938,2984,2946],{"class":1727},[938,2986,2673],{"class":944},[938,2988,1246],{"class":944},[938,2990,2991],{"class":940,"line":2454},[938,2992,2993],{"class":944},"    ],\n",[938,2995,2996,2998,3001,3003,3005,3007,3010,3012],{"class":940,"line":2460},[938,2997,2699],{"class":944},[938,2999,3000],{"class":1727},"genre",[938,3002,2673],{"class":944},[938,3004,1880],{"class":944},[938,3006,2635],{"class":944},[938,3008,3009],{"class":1727},"Pop",[938,3011,2673],{"class":944},[938,3013,2716],{"class":944},[938,3015,3016,3018,3021,3023,3025,3027,3030,3032],{"class":940,"line":2466},[938,3017,2699],{"class":944},[938,3019,3020],{"class":1727},"releaseDate",[938,3022,2673],{"class":944},[938,3024,1880],{"class":944},[938,3026,2635],{"class":944},[938,3028,3029],{"class":1727},"2026-04-01",[938,3031,2673],{"class":944},[938,3033,2716],{"class":944},[938,3035,3036,3038,3041,3043,3045],{"class":940,"line":2472},[938,3037,2699],{"class":944},[938,3039,3040],{"class":1727},"territories",[938,3042,2673],{"class":944},[938,3044,1880],{"class":944},[938,3046,1030],{"class":944},[938,3048,3050,3053,3056,3058,3060,3062,3065,3067],{"class":940,"line":3049},24,[938,3051,3052],{"class":944},"        \"",[938,3054,3055],{"class":1727},"include",[938,3057,2673],{"class":944},[938,3059,1880],{"class":944},[938,3061,2635],{"class":944},[938,3063,3064],{"class":1727},"Worldwide",[938,3066,2673],{"class":944},[938,3068,2716],{"class":944},[938,3070,3072,3074,3077,3079,3081,3084,3086,3089,3091,3093,3095,3098,3100,3102,3104,3107,3109,3111,3113,3116,3118,3120,3122,3125,3127,3129,3131,3134,3136,3138,3140,3143,3145],{"class":940,"line":3071},25,[938,3073,3052],{"class":944},[938,3075,3076],{"class":1727},"exclude",[938,3078,2673],{"class":944},[938,3080,1880],{"class":944},[938,3082,3083],{"class":944}," [",[938,3085,2673],{"class":944},[938,3087,3088],{"class":1727},"JP",[938,3090,2673],{"class":944},[938,3092,1085],{"class":944},[938,3094,2635],{"class":944},[938,3096,3097],{"class":1727},"DE",[938,3099,2673],{"class":944},[938,3101,1085],{"class":944},[938,3103,2635],{"class":944},[938,3105,3106],{"class":1727},"AT",[938,3108,2673],{"class":944},[938,3110,1085],{"class":944},[938,3112,2635],{"class":944},[938,3114,3115],{"class":1727},"CH",[938,3117,2673],{"class":944},[938,3119,1085],{"class":944},[938,3121,2635],{"class":944},[938,3123,3124],{"class":1727},"NL",[938,3126,2673],{"class":944},[938,3128,1085],{"class":944},[938,3130,2635],{"class":944},[938,3132,3133],{"class":1727},"AU",[938,3135,2673],{"class":944},[938,3137,1085],{"class":944},[938,3139,2635],{"class":944},[938,3141,3142],{"class":1727},"NZ",[938,3144,2673],{"class":944},[938,3146,2767],{"class":944},[938,3148,3150],{"class":940,"line":3149},26,[938,3151,1234],{"class":944},[938,3153,3155],{"class":940,"line":3154},27,[938,3156,2739],{"class":944},[938,3158,3160,3163,3165,3168,3170,3172,3174,3176,3179,3181],{"class":940,"line":3159},28,[938,3161,3162],{"class":955},"release_id ",[938,3164,1125],{"class":944},[938,3166,3167],{"class":955}," release",[938,3169,1148],{"class":944},[938,3171,2754],{"class":1072},[938,3173,2757],{"class":944},[938,3175,2673],{"class":944},[938,3177,3178],{"class":1727},"id",[938,3180,2673],{"class":944},[938,3182,2767],{"class":944},[938,3184,3186],{"class":940,"line":3185},29,[938,3187,1057],{"emptyLinePlaceholder":1056},[938,3189,3191],{"class":940,"line":3190},30,[938,3192,3193],{"class":1535},"# 3. Validate before delivery\n",[938,3195,3197,3200,3202,3204,3206,3208],{"class":940,"line":3196},31,[938,3198,3199],{"class":955},"validation ",[938,3201,1125],{"class":944},[938,3203,2660],{"class":955},[938,3205,1148],{"class":944},[938,3207,2665],{"class":1072},[938,3209,3210],{"class":944},"(\n",[938,3212,3214,3217,3219,3221,3223,3225,3228,3230,3233,3235,3238],{"class":940,"line":3213},32,[938,3215,3216],{"class":1035},"    f",[938,3218,2673],{"class":1727},[938,3220,2676],{"class":1522},[938,3222,2679],{"class":1072},[938,3224,2682],{"class":1522},[938,3226,3227],{"class":1727},"/distribution/release/",[938,3229,2676],{"class":1522},[938,3231,3232],{"class":1072},"release_id",[938,3234,2682],{"class":1522},[938,3236,3237],{"class":1727},"/validate\"",[938,3239,2716],{"class":944},[938,3241,3243,3246,3248],{"class":940,"line":3242},33,[938,3244,3245],{"class":2690},"    headers",[938,3247,1125],{"class":944},[938,3249,3250],{"class":1072},"headers\n",[938,3252,3254],{"class":940,"line":3253},34,[938,3255,3256],{"class":944},")\n",[938,3258,3260],{"class":940,"line":3259},35,[938,3261,1057],{"emptyLinePlaceholder":1056},[938,3263,3265],{"class":940,"line":3264},36,[938,3266,3267],{"class":1535},"# 4. Queue for delivery to DSPs\n",[938,3269,3271,3273,3276,3278,3280,3282,3284,3287,3289],{"class":940,"line":3270},37,[938,3272,1676],{"class":1097},[938,3274,3275],{"class":955}," validation",[938,3277,1148],{"class":944},[938,3279,2754],{"class":1072},[938,3281,2757],{"class":944},[938,3283,2673],{"class":944},[938,3285,3286],{"class":1727},"valid",[938,3288,2673],{"class":944},[938,3290,3291],{"class":944},"]:\n",[938,3293,3295,3298,3300,3302,3304,3306,3308,3310,3312,3314,3317],{"class":940,"line":3294},38,[938,3296,3297],{"class":955},"    httpx",[938,3299,1148],{"class":944},[938,3301,2665],{"class":1072},[938,3303,1076],{"class":944},[938,3305,2670],{"class":1035},[938,3307,2673],{"class":1727},[938,3309,2676],{"class":1522},[938,3311,2679],{"class":1072},[938,3313,2682],{"class":1522},[938,3315,3316],{"class":1727},"/distribution/release/addtoqueue\"",[938,3318,2716],{"class":944},[938,3320,3322,3325,3327,3329],{"class":940,"line":3321},39,[938,3323,3324],{"class":2690},"        headers",[938,3326,1125],{"class":944},[938,3328,2849],{"class":1072},[938,3330,2716],{"class":944},[938,3332,3334,3337,3340,3342,3345,3347,3349,3352],{"class":940,"line":3333},40,[938,3335,3336],{"class":2690},"        json",[938,3338,3339],{"class":944},"={",[938,3341,2673],{"class":944},[938,3343,3344],{"class":1727},"releaseId",[938,3346,2673],{"class":944},[938,3348,1880],{"class":944},[938,3350,3351],{"class":1072}," release_id",[938,3353,1246],{"class":944},[938,3355,3357],{"class":940,"line":3356},41,[938,3358,3359],{"class":944},"    )\n",[2195,3361,3362],{},[752,3363,3364,3365,3368,3369,3372],{},"This is a simplified example. In production, you'd also upload audio files via ",[920,3366,3367],{},"/media/audio/upload",", cover art via ",[920,3370,3371],{},"/media/image/upload",", and handle webhook callbacks for delivery status updates.",[767,3374],{},[747,3376,3378],{"id":3377},"ddex-as-the-backbone","DDEX as the Backbone",[752,3380,3381],{},"Under the hood, every delivery to a DSP is a DDEX ERN message - an XML package describing the release, its resources (audio, artwork), and the commercial terms (deals) under which DSPs can exploit the content.",[752,3383,3384],{},"An ERN message has three critical sections:",[774,3386,3388],{"id":3387},"resourcelist","ResourceList",[752,3390,3391],{},"Defines the audio files, cover art, and their metadata. Each sound recording includes an ISRC, title, artist credits, and territory-specific details:",[929,3393,3395],{"className":931,"code":3394,"language":933,"meta":934,"style":934},"\u003CSoundRecording>\n  \u003CSoundRecordingId>\n    \u003CISRC>SKXXX2500001\u003C/ISRC>\n  \u003C/SoundRecordingId>\n  \u003CResourceReference>A1\u003C/ResourceReference>\n  \u003CSoundRecordingDetailsByTerritory>\n    \u003CTerritoryCode>Worldwide\u003C/TerritoryCode>\n    \u003CTitle TitleType=\"FormalTitle\">\n      \u003CTitleText>Delilah\u003C/TitleText>\n    \u003C/Title>\n    \u003CDisplayArtist>\n      \u003CPartyName>\u003CFullName>MIKOLAS\u003C/FullName>\u003C/PartyName>\n      \u003CArtistRole>MainArtist\u003C/ArtistRole>\n    \u003C/DisplayArtist>\n  \u003C/SoundRecordingDetailsByTerritory>\n\u003C/SoundRecording>\n",[920,3396,3397,3406,3416,3435,3444,3462,3471,3488,3509,3528,3537,3546,3574,3591,3599,3607],{"__ignoreMap":934},[938,3398,3399,3401,3404],{"class":940,"line":941},[938,3400,945],{"class":944},[938,3402,3403],{"class":948},"SoundRecording",[938,3405,964],{"class":944},[938,3407,3408,3411,3414],{"class":940,"line":967},[938,3409,3410],{"class":944},"  \u003C",[938,3412,3413],{"class":948},"SoundRecordingId",[938,3415,964],{"class":944},[938,3417,3418,3421,3424,3426,3429,3431,3433],{"class":940,"line":1053},[938,3419,3420],{"class":944},"    \u003C",[938,3422,3423],{"class":948},"ISRC",[938,3425,952],{"class":944},[938,3427,3428],{"class":955},"SKXXX2500001",[938,3430,959],{"class":944},[938,3432,3423],{"class":948},[938,3434,964],{"class":944},[938,3436,3437,3440,3442],{"class":940,"line":1060},[938,3438,3439],{"class":944},"  \u003C/",[938,3441,3413],{"class":948},[938,3443,964],{"class":944},[938,3445,3446,3448,3451,3453,3456,3458,3460],{"class":940,"line":1066},[938,3447,3410],{"class":944},[938,3449,3450],{"class":948},"ResourceReference",[938,3452,952],{"class":944},[938,3454,3455],{"class":955},"A1",[938,3457,959],{"class":944},[938,3459,3450],{"class":948},[938,3461,964],{"class":944},[938,3463,3464,3466,3469],{"class":940,"line":1094},[938,3465,3410],{"class":944},[938,3467,3468],{"class":948},"SoundRecordingDetailsByTerritory",[938,3470,964],{"class":944},[938,3472,3473,3475,3478,3480,3482,3484,3486],{"class":940,"line":1116},[938,3474,3420],{"class":944},[938,3476,3477],{"class":948},"TerritoryCode",[938,3479,952],{"class":944},[938,3481,3064],{"class":955},[938,3483,959],{"class":944},[938,3485,3477],{"class":948},[938,3487,964],{"class":944},[938,3489,3490,3492,3495,3498,3500,3502,3505,3507],{"class":940,"line":1134},[938,3491,3420],{"class":944},[938,3493,3494],{"class":948},"Title",[938,3496,3497],{"class":1035}," TitleType",[938,3499,1125],{"class":944},[938,3501,2673],{"class":944},[938,3503,3504],{"class":1727},"FormalTitle",[938,3506,2673],{"class":944},[938,3508,964],{"class":944},[938,3510,3511,3514,3517,3519,3522,3524,3526],{"class":940,"line":1154},[938,3512,3513],{"class":944},"      \u003C",[938,3515,3516],{"class":948},"TitleText",[938,3518,952],{"class":944},[938,3520,3521],{"class":955},"Delilah",[938,3523,959],{"class":944},[938,3525,3516],{"class":948},[938,3527,964],{"class":944},[938,3529,3530,3533,3535],{"class":940,"line":1171},[938,3531,3532],{"class":944},"    \u003C/",[938,3534,3494],{"class":948},[938,3536,964],{"class":944},[938,3538,3539,3541,3544],{"class":940,"line":1191},[938,3540,3420],{"class":944},[938,3542,3543],{"class":948},"DisplayArtist",[938,3545,964],{"class":944},[938,3547,3548,3550,3553,3556,3559,3561,3563,3565,3567,3570,3572],{"class":940,"line":1196},[938,3549,3513],{"class":944},[938,3551,3552],{"class":948},"PartyName",[938,3554,3555],{"class":944},">\u003C",[938,3557,3558],{"class":948},"FullName",[938,3560,952],{"class":944},[938,3562,2928],{"class":955},[938,3564,959],{"class":944},[938,3566,3558],{"class":948},[938,3568,3569],{"class":944},">\u003C/",[938,3571,3552],{"class":948},[938,3573,964],{"class":944},[938,3575,3576,3578,3581,3583,3585,3587,3589],{"class":940,"line":1231},[938,3577,3513],{"class":944},[938,3579,3580],{"class":948},"ArtistRole",[938,3582,952],{"class":944},[938,3584,2946],{"class":955},[938,3586,959],{"class":944},[938,3588,3580],{"class":948},[938,3590,964],{"class":944},[938,3592,3593,3595,3597],{"class":940,"line":1237},[938,3594,3532],{"class":944},[938,3596,3543],{"class":948},[938,3598,964],{"class":944},[938,3600,3601,3603,3605],{"class":940,"line":1243},[938,3602,3439],{"class":944},[938,3604,3468],{"class":948},[938,3606,964],{"class":944},[938,3608,3609,3611,3613],{"class":940,"line":2431},[938,3610,959],{"class":944},[938,3612,3403],{"class":948},[938,3614,964],{"class":944},[774,3616,3618],{"id":3617},"releaselist","ReleaseList",[752,3620,3621],{},"Defines the product - the album or single - and links it to its component resources:",[929,3623,3625],{"className":931,"code":3624,"language":933,"meta":934,"style":934},"\u003CRelease>\n  \u003CReleaseId>\u003CICPN>0123456789012\u003C/ICPN>\u003C/ReleaseId>\n  \u003CReleaseReference>R0\u003C/ReleaseReference>\n  \u003CReleaseType>Album\u003C/ReleaseType>\n  \u003CReleaseDetailsByTerritory>\n    \u003CTerritoryCode>Worldwide\u003C/TerritoryCode>\n    \u003CDisplayArtistName>MIKOLAS\u003C/DisplayArtistName>\n    \u003CTitle TitleType=\"FormalTitle\">\n      \u003CTitleText>ONE\u003C/TitleText>\n    \u003C/Title>\n  \u003C/ReleaseDetailsByTerritory>\n\u003C/Release>\n",[920,3626,3627,3636,3663,3681,3699,3708,3724,3741,3759,3776,3784,3792],{"__ignoreMap":934},[938,3628,3629,3631,3634],{"class":940,"line":941},[938,3630,945],{"class":944},[938,3632,3633],{"class":948},"Release",[938,3635,964],{"class":944},[938,3637,3638,3640,3643,3645,3648,3650,3653,3655,3657,3659,3661],{"class":940,"line":967},[938,3639,3410],{"class":944},[938,3641,3642],{"class":948},"ReleaseId",[938,3644,3555],{"class":944},[938,3646,3647],{"class":948},"ICPN",[938,3649,952],{"class":944},[938,3651,3652],{"class":955},"0123456789012",[938,3654,959],{"class":944},[938,3656,3647],{"class":948},[938,3658,3569],{"class":944},[938,3660,3642],{"class":948},[938,3662,964],{"class":944},[938,3664,3665,3667,3670,3672,3675,3677,3679],{"class":940,"line":1053},[938,3666,3410],{"class":944},[938,3668,3669],{"class":948},"ReleaseReference",[938,3671,952],{"class":944},[938,3673,3674],{"class":955},"R0",[938,3676,959],{"class":944},[938,3678,3669],{"class":948},[938,3680,964],{"class":944},[938,3682,3683,3685,3688,3690,3693,3695,3697],{"class":940,"line":1060},[938,3684,3410],{"class":944},[938,3686,3687],{"class":948},"ReleaseType",[938,3689,952],{"class":944},[938,3691,3692],{"class":955},"Album",[938,3694,959],{"class":944},[938,3696,3687],{"class":948},[938,3698,964],{"class":944},[938,3700,3701,3703,3706],{"class":940,"line":1066},[938,3702,3410],{"class":944},[938,3704,3705],{"class":948},"ReleaseDetailsByTerritory",[938,3707,964],{"class":944},[938,3709,3710,3712,3714,3716,3718,3720,3722],{"class":940,"line":1094},[938,3711,3420],{"class":944},[938,3713,3477],{"class":948},[938,3715,952],{"class":944},[938,3717,3064],{"class":955},[938,3719,959],{"class":944},[938,3721,3477],{"class":948},[938,3723,964],{"class":944},[938,3725,3726,3728,3731,3733,3735,3737,3739],{"class":940,"line":1116},[938,3727,3420],{"class":944},[938,3729,3730],{"class":948},"DisplayArtistName",[938,3732,952],{"class":944},[938,3734,2928],{"class":955},[938,3736,959],{"class":944},[938,3738,3730],{"class":948},[938,3740,964],{"class":944},[938,3742,3743,3745,3747,3749,3751,3753,3755,3757],{"class":940,"line":1134},[938,3744,3420],{"class":944},[938,3746,3494],{"class":948},[938,3748,3497],{"class":1035},[938,3750,1125],{"class":944},[938,3752,2673],{"class":944},[938,3754,3504],{"class":1727},[938,3756,2673],{"class":944},[938,3758,964],{"class":944},[938,3760,3761,3763,3765,3767,3770,3772,3774],{"class":940,"line":1154},[938,3762,3513],{"class":944},[938,3764,3516],{"class":948},[938,3766,952],{"class":944},[938,3768,3769],{"class":955},"ONE",[938,3771,959],{"class":944},[938,3773,3516],{"class":948},[938,3775,964],{"class":944},[938,3777,3778,3780,3782],{"class":940,"line":1171},[938,3779,3532],{"class":944},[938,3781,3494],{"class":948},[938,3783,964],{"class":944},[938,3785,3786,3788,3790],{"class":940,"line":1191},[938,3787,3439],{"class":944},[938,3789,3705],{"class":948},[938,3791,964],{"class":944},[938,3793,3794,3796,3798],{"class":940,"line":1196},[938,3795,959],{"class":944},[938,3797,3633],{"class":948},[938,3799,964],{"class":944},[774,3801,3803],{"id":3802},"deallist","DealList",[752,3805,3806,3807,3810],{},"The ",[755,3808,3809],{},"only"," section that grants commercial rights. This is where territory restrictions live:",[929,3812,3814],{"className":931,"code":3813,"language":933,"meta":934,"style":934},"\u003CReleaseDeal>\n  \u003CDealReleaseReference>R0\u003C/DealReleaseReference>\n  \u003CDeal>\n    \u003CDealTerms>\n      \u003CTerritoryCode>Worldwide\u003C/TerritoryCode>\n      \u003CExcludedTerritoryCode>JP\u003C/ExcludedTerritoryCode>\n      \u003CCommercialModelType>SubscriptionModel\u003C/CommercialModelType>\n      \u003CUsage>\n        \u003CUseType>OnDemandStream\u003C/UseType>\n      \u003C/Usage>\n      \u003CValidityPeriod>\n        \u003CStartDate>2026-03-15\u003C/StartDate>\n      \u003C/ValidityPeriod>\n    \u003C/DealTerms>\n  \u003C/Deal>\n\u003C/ReleaseDeal>\n",[920,3815,3816,3825,3842,3851,3860,3876,3893,3911,3920,3939,3948,3957,3975,3983,3991,3999],{"__ignoreMap":934},[938,3817,3818,3820,3823],{"class":940,"line":941},[938,3819,945],{"class":944},[938,3821,3822],{"class":948},"ReleaseDeal",[938,3824,964],{"class":944},[938,3826,3827,3829,3832,3834,3836,3838,3840],{"class":940,"line":967},[938,3828,3410],{"class":944},[938,3830,3831],{"class":948},"DealReleaseReference",[938,3833,952],{"class":944},[938,3835,3674],{"class":955},[938,3837,959],{"class":944},[938,3839,3831],{"class":948},[938,3841,964],{"class":944},[938,3843,3844,3846,3849],{"class":940,"line":1053},[938,3845,3410],{"class":944},[938,3847,3848],{"class":948},"Deal",[938,3850,964],{"class":944},[938,3852,3853,3855,3858],{"class":940,"line":1060},[938,3854,3420],{"class":944},[938,3856,3857],{"class":948},"DealTerms",[938,3859,964],{"class":944},[938,3861,3862,3864,3866,3868,3870,3872,3874],{"class":940,"line":1066},[938,3863,3513],{"class":944},[938,3865,3477],{"class":948},[938,3867,952],{"class":944},[938,3869,3064],{"class":955},[938,3871,959],{"class":944},[938,3873,3477],{"class":948},[938,3875,964],{"class":944},[938,3877,3878,3880,3883,3885,3887,3889,3891],{"class":940,"line":1094},[938,3879,3513],{"class":944},[938,3881,3882],{"class":948},"ExcludedTerritoryCode",[938,3884,952],{"class":944},[938,3886,3088],{"class":955},[938,3888,959],{"class":944},[938,3890,3882],{"class":948},[938,3892,964],{"class":944},[938,3894,3895,3897,3900,3902,3905,3907,3909],{"class":940,"line":1116},[938,3896,3513],{"class":944},[938,3898,3899],{"class":948},"CommercialModelType",[938,3901,952],{"class":944},[938,3903,3904],{"class":955},"SubscriptionModel",[938,3906,959],{"class":944},[938,3908,3899],{"class":948},[938,3910,964],{"class":944},[938,3912,3913,3915,3918],{"class":940,"line":1134},[938,3914,3513],{"class":944},[938,3916,3917],{"class":948},"Usage",[938,3919,964],{"class":944},[938,3921,3922,3925,3928,3930,3933,3935,3937],{"class":940,"line":1154},[938,3923,3924],{"class":944},"        \u003C",[938,3926,3927],{"class":948},"UseType",[938,3929,952],{"class":944},[938,3931,3932],{"class":955},"OnDemandStream",[938,3934,959],{"class":944},[938,3936,3927],{"class":948},[938,3938,964],{"class":944},[938,3940,3941,3944,3946],{"class":940,"line":1171},[938,3942,3943],{"class":944},"      \u003C/",[938,3945,3917],{"class":948},[938,3947,964],{"class":944},[938,3949,3950,3952,3955],{"class":940,"line":1191},[938,3951,3513],{"class":944},[938,3953,3954],{"class":948},"ValidityPeriod",[938,3956,964],{"class":944},[938,3958,3959,3961,3964,3966,3969,3971,3973],{"class":940,"line":1196},[938,3960,3924],{"class":944},[938,3962,3963],{"class":948},"StartDate",[938,3965,952],{"class":944},[938,3967,3968],{"class":955},"2026-03-15",[938,3970,959],{"class":944},[938,3972,3963],{"class":948},[938,3974,964],{"class":944},[938,3976,3977,3979,3981],{"class":940,"line":1231},[938,3978,3943],{"class":944},[938,3980,3954],{"class":948},[938,3982,964],{"class":944},[938,3984,3985,3987,3989],{"class":940,"line":1237},[938,3986,3532],{"class":944},[938,3988,3857],{"class":948},[938,3990,964],{"class":944},[938,3992,3993,3995,3997],{"class":940,"line":1243},[938,3994,3439],{"class":944},[938,3996,3848],{"class":948},[938,3998,964],{"class":944},[938,4000,4001,4003,4005],{"class":940,"line":2431},[938,4002,959],{"class":944},[938,4004,3822],{"class":948},[938,4006,964],{"class":944},[2204,4008,4009],{},[752,4010,4011,4014,4015,4018,4019,4022,4023,4025,4026,4029],{},[755,4012,4013],{},"Common DDEX mistake:"," ",[920,4016,4017],{},"DetailsByTerritory"," in ResourceList and ReleaseList describes ",[2032,4020,4021],{},"how content is presented"," in different markets. The ",[920,4024,3803],{}," defines ",[2032,4027,4028],{},"where content is available",". Confusing these two - applying territory restrictions in metadata instead of deals - is one of the most frequent implementation errors.",[774,4031,4033],{"id":4032},"territory-restrictions-in-practice","Territory Restrictions in Practice",[752,4035,4036,4037,1880],{},"When a distributor has agreements with partners who hold rights in specific regions, individual tracks must be excluded from those territories. This requires consistent territory entries across ",[755,4038,4039],{},"three XML sections",[4041,4042,4043,4048,4053],"ol",{},[782,4044,4045,4047],{},[920,4046,3468],{}," in ResourceList",[782,4049,4050,4052],{},[920,4051,3705],{}," in ReleaseList",[782,4054,4055,4057],{},[920,4056,3857],{}," in DealList",[752,4059,4060,4061,4064],{},"Platforms like Revelator handle this at the release level through their UI, but ",[755,4062,4063],{},"track-level territory restrictions within an album"," are where the friction begins. This is exactly the kind of limitation that pushes distributors toward a custom frontend - you model the territory logic in your own system and push the correct per-track restrictions through the API.",[767,4066],{},[747,4068,4070],{"id":4069},"real-world-gotchas","Real-World Gotchas",[752,4072,4073],{},"Having worked with distributors who use Revelator as their delivery backbone, here are the practical challenges we've encountered:",[2204,4075,4076],{},[752,4077,4078,4081],{},[755,4079,4080],{},"1. Mandatory Human Approval Step","\nRevelator requires a parent account to approve distributions before they reach DSPs. There is no fully automated end-to-end delivery without a human clicking \"approve\" in the web UI. For high-volume operations, this creates a bottleneck that API-only workflows cannot bypass.",[774,4083,4085],{"id":4084},"_2-full-object-re-submission","2. Full Object Re-submission",[752,4087,4088],{},"There is no PATCH support for releases. Editing a release requires re-submitting the full release object. If you omit files, they are auto-deleted. This means your backend must always maintain the complete release state.",[774,4090,4092],{"id":4091},"_3-breaking-api-changes","3. Breaking API Changes",[752,4094,4095],{},"Revelator's API has had several breaking changes in 2025-2026:",[1906,4097,4098,4108],{},[1909,4099,4100],{},[1912,4101,4102,4105],{},[1915,4103,4104],{},"Change",[1915,4106,4107],{},"When",[1922,4109,4110,4124,4135,4143],{},[1912,4111,4112,4121],{},[1927,4113,4114,4115,4118,4119],{},"UPC type changed from ",[920,4116,4117],{},"number"," to ",[920,4120,972],{},[1927,4122,4123],{},"Feb 2026",[1912,4125,4126,4132],{},[1927,4127,4128,4129],{},"API base URL migrated to ",[920,4130,4131],{},"api.revelator.com",[1927,4133,4134],{},"Mar 2026",[1912,4136,4137,4140],{},[1927,4138,4139],{},"Production/engineering credits became mandatory",[1927,4141,4142],{},"Jun 2025",[1912,4144,4145,4148],{},[1927,4146,4147],{},"Zero-sentinel IDs deprecated",[1927,4149,4150],{},"Nov 2025",[752,4152,4153],{},"Your integration layer needs resilience against schema drift.",[774,4155,4157],{"id":4156},"_4-dsp-specific-ddex-interpretation","4. DSP-Specific DDEX Interpretation",[752,4159,4160],{},"Even when Revelator generates valid ERN XML, each DSP interprets certain elements differently. Spotify's ingestion engine, Apple's Content Provider system, and Amazon's pipeline each have proprietary extensions and quirks. When delivery fails, debugging requires understanding both the DDEX standard and the specific DSP's interpretation.",[774,4162,4164],{"id":4163},"_5-locked-distribution-states","5. Locked Distribution States",[752,4166,4167],{},"Tracks in certain distribution statuses (-10, -11, -20, -21) become read-only. If you need to modify metadata on a locked track, you may need to initiate a takedown and redeliver, adding complexity to your workflow engine.",[767,4169],{},[747,4171,4173],{"id":4172},"when-does-option-a-become-a-dead-end","When Does Option A Become a Dead End?",[752,4175,4176],{},"The hybrid approach works well when:",[779,4178,4179,4185,4191,4194],{},[782,4180,4181,4182],{},"Your catalog is under ",[755,4183,4184],{},"50,000 releases",[782,4186,4187,4188],{},"You have fewer than ",[755,4189,4190],{},"30 DSP targets",[782,4192,4193],{},"Territory complexity is moderate (album-level restrictions, not per-track-per-DSP)",[782,4195,4196],{},"Royalty reporting needs are standard (DSP-level aggregates, not sub-publishing splits)",[752,4198,4199],{},"It starts breaking down when:",[1906,4201,4202,4212],{},[1909,4203,4204],{},[1912,4205,4206,4209],{},[1915,4207,4208],{},"Signal",[1915,4210,4211],{},"Why It Matters",[1922,4213,4214,4224,4234,4244],{},[1912,4215,4216,4221],{},[1927,4217,4218],{},[755,4219,4220],{},"Royalty logic becomes the product",[1927,4222,4223],{},"If your competitive advantage is in how you calculate, split, and report royalties - with advances, recoupables, and multi-party splits - you'll outgrow Revelator's royalty engine before its delivery",[1912,4225,4226,4231],{},[1927,4227,4228],{},[755,4229,4230],{},"You need real-time delivery control",[1927,4232,4233],{},"The mandatory approval step and lack of granular delivery status callbacks limit automation at scale",[1912,4235,4236,4241],{},[1927,4237,4238],{},[755,4239,4240],{},"DSP-specific customization matters",[1927,4242,4243],{},"If you need different DDEX profiles per DSP (ERN 3.8.2 for legacy, ERN 4.3 for Spotify), you'll need your own generation pipeline",[1912,4245,4246,4251],{},[1927,4247,4248],{},[755,4249,4250],{},"Vendor risk becomes unacceptable",[1927,4252,4253],{},"Revelator is VC-backed. API changes, pricing shifts, or an acquisition could force migration under pressure",[2195,4255,4256],{},[752,4257,4258,4259,4262],{},"The practical tipping point is usually ",[755,4260,4261],{},"2-3 years"," into the hybrid approach, when the workarounds and API limitations start costing more in engineering time than building the replaced components would.",[767,4264],{},[747,4266,4268],{"id":4267},"the-hybrid-to-independent-migration-path","The Hybrid-to-Independent Migration Path",[752,4270,4271],{},"The smartest architecture for Option A anticipates Option B:",[4041,4273,4274,4280,4286,4292],{},[782,4275,4276,4279],{},[755,4277,4278],{},"Own your metadata."," Never treat Revelator as the source of truth. Your database is canonical; Revelator is a sync target.",[782,4281,4282,4285],{},[755,4283,4284],{},"Abstract the delivery layer."," Build an internal delivery interface that Revelator implements today but could be swapped for direct DSP integrations tomorrow.",[782,4287,4288,4291],{},[755,4289,4290],{},"Build your own rights engine from day one."," Territory restrictions, split sheets, and rights ownership are your core IP. Never delegate this logic to the platform.",[782,4293,4294,4297],{},[755,4295,4296],{},"Invest in DDEX competency."," Understanding ERN generation and validation - even if Revelator handles it today - is essential knowledge for the eventual transition.",[2330,4299,4300],{},[752,4301,4302,4303,4306],{},"This way, when Option A hits its ceiling, you migrate ",[755,4304,4305],{},"component by component"," rather than executing a risky big-bang rewrite.",[767,4308],{},[747,4310,4312],{"id":4311},"conclusion","Conclusion",[752,4314,4315],{},"For distributors with direct DSP contracts who are hitting the limits of their current platform, the hybrid approach - custom frontend on Revelator's API - is the fastest path to operational control without the multi-year investment of going fully independent.",[752,4317,4318],{},"The key is to build it with migration in mind. Own your metadata, abstract your delivery layer, and invest in DDEX expertise. When the time comes to replace the backend, you'll be swapping a component rather than rebuilding a platform.",[767,4320],{},[747,4322,4324],{"id":4323},"resources","Resources",[779,4326,4327,4334,4341,4348,4352,4357],{},[782,4328,4329],{},[2036,4330,4333],{"href":4331,"rel":4332},"https://api-docs.revelator.com",[2040],"Revelator API Documentation",[782,4335,4336],{},[2036,4337,4340],{"href":4338,"rel":4339},"https://kb.ddex.net/implementing-each-standard/electronic-release-notification-message-suite-(ern)/",[2040],"DDEX ERN Knowledge Base",[782,4342,4343],{},[2036,4344,4347],{"href":4345,"rel":4346},"https://ddexvalidator.musictechlab.io/",[2040],"MTL DDEX Validator",[782,4349,4350],{},[2036,4351,136],{"href":137},[782,4353,4354],{},[2036,4355,4356],{"href":161},"Introduction to Generating DDEX Files Using Python",[782,4358,4359,4362],{},[2036,4360,4361],{"href":85},"AI-Powered Analytics Dashboard"," - once distribution data flows into your analytics pipeline, make it queryable with natural language",[2043,4364,4365],{},"html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}",{"title":934,"searchDepth":967,"depth":967,"links":4367},[4368,4369,4370,4377,4382,4388,4394,4395,4396,4397],{"id":2123,"depth":967,"text":2124},{"id":2189,"depth":967,"text":2190},{"id":2223,"depth":967,"text":2224,"children":4371},[4372,4373,4374,4375,4376],{"id":2233,"depth":1053,"text":2234},{"id":2254,"depth":1053,"text":2108},{"id":2286,"depth":1053,"text":2287},{"id":2301,"depth":1053,"text":2302},{"id":2316,"depth":1053,"text":2317},{"id":2343,"depth":967,"text":2344,"children":4378},[4379,4380,4381],{"id":2478,"depth":1053,"text":2479},{"id":2546,"depth":1053,"text":2547},{"id":2602,"depth":1053,"text":2603},{"id":3377,"depth":967,"text":3378,"children":4383},[4384,4385,4386,4387],{"id":3387,"depth":1053,"text":3388},{"id":3617,"depth":1053,"text":3618},{"id":3802,"depth":1053,"text":3803},{"id":4032,"depth":1053,"text":4033},{"id":4069,"depth":967,"text":4070,"children":4389},[4390,4391,4392,4393],{"id":4084,"depth":1053,"text":4085},{"id":4091,"depth":1053,"text":4092},{"id":4156,"depth":1053,"text":4157},{"id":4163,"depth":1053,"text":4164},{"id":4172,"depth":967,"text":4173},{"id":4267,"depth":967,"text":4268},{"id":4311,"depth":967,"text":4312},{"id":4323,"depth":967,"text":4324},"music-data","2026-02-17T00:00:00.000Z","A practical guide for music distributors evaluating a hybrid approach: custom frontend with Revelator's API as the delivery backbone. Covers architecture, DDEX integration, territory handling, and when to go fully independent.",{"src":4402},"/images/blog/musictechlab_blog_building-a-custom-music-delivery-platform-on-the-revelator-api.webp",{"enabled":1056,"items":4404},[4405,4408,4411,4414],{"text":4406,"icon":4407},"Revelator claims 100+ DSP integrations but requires a manual approval step before delivery.","i-lucide-code",{"text":4409,"icon":4410},"Territory restrictions must be consistent across three XML sections or deliveries fail silently.","i-lucide-alert-triangle",{"text":4412,"icon":4413},"Own your metadata from day one and treat Revelator as a sync target, not the source of truth.","i-lucide-database",{"text":4415,"icon":4416},"The hybrid approach typically hits its ceiling after 2-3 years of scaling.","i-lucide-trending-up",{},{"title":112,"description":4400},[4398,2094,4420,4421],"API","royalties","j-0HOY7SF2pZKgaTUKG4xI6pYGGOIVWAd9EiQc5LjGU",{"id":4424,"title":558,"authors":4425,"badge":4428,"body":4431,"category":11589,"client":742,"date":11590,"description":11591,"extension":2074,"faq":742,"featured":69,"featuredOrder":742,"hidden":69,"image":11592,"keyTakeaways":11595,"meta":11607,"navigation":1056,"path":559,"seo":11608,"status":742,"stem":560,"tags":11609,"teaser":742,"__hash__":11610,"score":941},"posts/blog/software-development/integrating-signnow-e-signatures-into-your-django-application.md",[4426],{"name":738,"to":739,"avatar":4427},{"src":741},{"label":4429,"color":4430},"API Integration","#6366f1",{"type":744,"value":4432,"toc":11565},[4433,4436,4451,4454,4458,4461,4535,4538,4542,4545,4575,4578,4615,4619,4648,4655,4729,4743,4746,4951,4955,4961,4965,4968,5913,5935,5939,5942,6686,6690,6693,6707,6710,7110,7114,7117,7524,7528,7538,8281,8284,8292,8296,8299,9603,9624,9628,9631,10327,10332,10335,10434,10437,10490,10494,10497,10882,10886,10889,11361,11365,11368,11372,11382,11386,11389,11393,11403,11407,11410,11414,11426,11428,11431,11555,11562],[752,4434,4435],{},"Electronic signatures have gone from \"nice to have\" to a hard requirement for any platform that deals with contracts, agreements, or compliance documents. Whether you're onboarding beta testers, sending NDAs, or processing deposit confirmations, manually emailing PDFs and chasing wet signatures simply doesn't scale.",[752,4437,4438,4439,4444,4445,4450],{},"At ",[2036,4440,4443],{"href":4441,"rel":4442},"https://beatbuddy.pro",[2040],"BeatBuddy",", we needed a way to automatically send documents for signature as part of our tester onboarding flow - without leaving the Django admin. We chose ",[2036,4446,4449],{"href":4447,"rel":4448},"https://www.signnow.com/developers",[2040],"airSlate SignNow"," for its developer-friendly REST API, generous sandbox environment, and competitive pricing.",[752,4452,4453],{},"This article walks through how we built the integration end-to-end: authenticating with OAuth2, uploading documents, sending signing invites, handling webhooks, and managing the full document lifecycle - all from within a Django + Celery stack.",[747,4455,4457],{"id":4456},"why-signnow","Why SignNow?",[752,4459,4460],{},"Before diving into code, here's why we picked SignNow over alternatives like DocuSign or HelloSign:",[1906,4462,4463,4473],{},[1909,4464,4465],{},[1912,4466,4467,4470],{},[1915,4468,4469],{},"Criteria",[1915,4471,4472],{},"SignNow",[1922,4474,4475,4485,4495,4505,4515,4525],{},[1912,4476,4477,4482],{},[1927,4478,4479],{},[755,4480,4481],{},"Sandbox",[1927,4483,4484],{},"Free, 2,000 signature invites for testing",[1912,4486,4487,4492],{},[1927,4488,4489],{},[755,4490,4491],{},"API style",[1927,4493,4494],{},"Clean REST API with JSON payloads",[1912,4496,4497,4502],{},[1927,4498,4499],{},[755,4500,4501],{},"Authentication",[1927,4503,4504],{},"Standard OAuth2 (password grant)",[1912,4506,4507,4512],{},[1927,4508,4509],{},[755,4510,4511],{},"Webhooks",[1927,4513,4514],{},"Per-document event subscriptions",[1912,4516,4517,4522],{},[1927,4518,4519],{},[755,4520,4521],{},"Pricing",[1927,4523,4524],{},"Significantly cheaper than DocuSign at scale",[1912,4526,4527,4532],{},[1927,4528,4529],{},[755,4530,4531],{},"SDKs",[1927,4533,4534],{},"Official SDKs for Python, Node.js, PHP, Java, C#",[752,4536,4537],{},"For our use case - programmatically sending documents for a single signer - SignNow's API was straightforward and well-documented.",[747,4539,4541],{"id":4540},"architecture-overview","Architecture overview",[752,4543,4544],{},"Here's how the integration fits into our Django application:",[929,4546,4548],{"className":2350,"code":4547,"language":2352,"meta":934,"style":934},"flowchart TD\n    A[\"Django Admin (trigger sign)\"] --> B[\"Celery Worker (async pipeline)\"]\n    B --> C[\"SignNow API (upload, sign)\"]\n    C -- \"webhook\" --> D[\"Webhook View (POST receiver)\"]\n    D --> E[\"Celery Worker (download PDF)\"]\n",[920,4549,4550,4555,4560,4565,4570],{"__ignoreMap":934},[938,4551,4552],{"class":940,"line":941},[938,4553,4554],{"class":955},"flowchart TD\n",[938,4556,4557],{"class":940,"line":967},[938,4558,4559],{"class":955},"    A[\"Django Admin (trigger sign)\"] --> B[\"Celery Worker (async pipeline)\"]\n",[938,4561,4562],{"class":940,"line":1053},[938,4563,4564],{"class":955},"    B --> C[\"SignNow API (upload, sign)\"]\n",[938,4566,4567],{"class":940,"line":1060},[938,4568,4569],{"class":955},"    C -- \"webhook\" --> D[\"Webhook View (POST receiver)\"]\n",[938,4571,4572],{"class":940,"line":1066},[938,4573,4574],{"class":955},"    D --> E[\"Celery Worker (download PDF)\"]\n",[752,4576,4577],{},"The key design decisions:",[4041,4579,4580,4586,4599,4605],{},[782,4581,4582,4585],{},[755,4583,4584],{},"Async everything"," - All SignNow API calls happen in Celery tasks, never in the request cycle",[782,4587,4588,4591,4592,4595,4596],{},[755,4589,4590],{},"Generic relations"," - The signing tracker (",[920,4593,4594],{},"SignableDocument",") can attach to any Django model via ",[920,4597,4598],{},"ContentType",[782,4600,4601,4604],{},[755,4602,4603],{},"Idempotent operations"," - The pipeline gracefully handles retries and duplicate webhook events",[782,4606,4607,4610,4611,4614],{},[755,4608,4609],{},"Service layer pattern"," - A thin ",[920,4612,4613],{},"SignNowService"," class wraps all raw API calls",[747,4616,4618],{"id":4617},"step-1-get-your-signnow-api-credentials","Step 1: Get your SignNow API credentials",[4041,4620,4621,4628,4638],{},[782,4622,4623,4624],{},"Create a free sandbox account at ",[2036,4625,4627],{"href":4447,"rel":4626},[2040],"signnow.com/developers",[782,4629,4630,4631,4634,4635],{},"In the API dashboard, create a new application to get your ",[755,4632,4633],{},"Client ID"," and ",[755,4636,4637],{},"Client Secret",[782,4639,4640,4641,4644,4645,1731],{},"Note your sandbox base URL: ",[920,4642,4643],{},"https://api-eval.signnow.com"," (production uses ",[920,4646,4647],{},"https://api.signnow.com",[752,4649,4650,4651,4654],{},"Add these to your ",[920,4652,4653],{},".env"," file:",[929,4656,4660],{"className":4657,"code":4658,"language":4659,"meta":934,"style":934},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# SignNow e-signature (https://www.signnow.com/developers)\nSIGNNOW_API_BASE_URL=https://api-eval.signnow.com  # Use api.signnow.com for production\nSIGNNOW_BASIC_AUTH=  # Base64-encoded client_id:client_secret\nSIGNNOW_USERNAME=    # Your SignNow account email\nSIGNNOW_PASSWORD=    # Your SignNow account password\nSIGNNOW_WEBHOOK_SECRET=  # For verifying webhook payloads\nSIGNNOW_WEBHOOK_CALLBACK_URL=  # Your public webhook endpoint\n","bash",[920,4661,4662,4667,4679,4689,4699,4709,4719],{"__ignoreMap":934},[938,4663,4664],{"class":940,"line":941},[938,4665,4666],{"class":1535},"# SignNow e-signature (https://www.signnow.com/developers)\n",[938,4668,4669,4672,4674,4676],{"class":940,"line":967},[938,4670,4671],{"class":955},"SIGNNOW_API_BASE_URL",[938,4673,1125],{"class":944},[938,4675,4643],{"class":1727},[938,4677,4678],{"class":1535},"  # Use api.signnow.com for production\n",[938,4680,4681,4684,4686],{"class":940,"line":1053},[938,4682,4683],{"class":955},"SIGNNOW_BASIC_AUTH",[938,4685,1125],{"class":944},[938,4687,4688],{"class":1535},"  # Base64-encoded client_id:client_secret\n",[938,4690,4691,4694,4696],{"class":940,"line":1060},[938,4692,4693],{"class":955},"SIGNNOW_USERNAME",[938,4695,1125],{"class":944},[938,4697,4698],{"class":1535},"    # Your SignNow account email\n",[938,4700,4701,4704,4706],{"class":940,"line":1066},[938,4702,4703],{"class":955},"SIGNNOW_PASSWORD",[938,4705,1125],{"class":944},[938,4707,4708],{"class":1535},"    # Your SignNow account password\n",[938,4710,4711,4714,4716],{"class":940,"line":1094},[938,4712,4713],{"class":955},"SIGNNOW_WEBHOOK_SECRET",[938,4715,1125],{"class":944},[938,4717,4718],{"class":1535},"  # For verifying webhook payloads\n",[938,4720,4721,4724,4726],{"class":940,"line":1116},[938,4722,4723],{"class":955},"SIGNNOW_WEBHOOK_CALLBACK_URL",[938,4725,1125],{"class":944},[938,4727,4728],{"class":1535},"  # Your public webhook endpoint\n",[2204,4730,4731],{},[752,4732,3806,4733,4735,4736,4739,4740],{},[920,4734,4683],{}," value must be the Base64 encoding of ",[920,4737,4738],{},"client_id:client_secret",". You can generate it with: ",[920,4741,4742],{},"echo -n \"your_client_id:your_client_secret\" | base64",[752,4744,4745],{},"Load these in your Django settings:",[929,4747,4750],{"className":2609,"code":4748,"filename":4749,"language":2611,"meta":934,"style":934},"# SignNow e-signature\nSIGNNOW_API_BASE_URL = env.str(\"SIGNNOW_API_BASE_URL\", default=\"https://api.signnow.com\")\nSIGNNOW_BASIC_AUTH = env.str(\"SIGNNOW_BASIC_AUTH\", default=\"\")\nSIGNNOW_USERNAME = env.str(\"SIGNNOW_USERNAME\", default=\"\")\nSIGNNOW_PASSWORD = env.str(\"SIGNNOW_PASSWORD\", default=\"\")\nSIGNNOW_WEBHOOK_SECRET = env.str(\"SIGNNOW_WEBHOOK_SECRET\", default=\"\")\nSIGNNOW_WEBHOOK_CALLBACK_URL = env.str(\"SIGNNOW_WEBHOOK_CALLBACK_URL\", default=\"\")\n","settings/base.py",[920,4751,4752,4757,4795,4827,4858,4889,4920],{"__ignoreMap":934},[938,4753,4754],{"class":940,"line":941},[938,4755,4756],{"class":1535},"# SignNow e-signature\n",[938,4758,4759,4762,4764,4767,4769,4772,4774,4776,4778,4780,4782,4785,4787,4789,4791,4793],{"class":940,"line":967},[938,4760,4761],{"class":955},"SIGNNOW_API_BASE_URL ",[938,4763,1125],{"class":944},[938,4765,4766],{"class":955}," env",[938,4768,1148],{"class":944},[938,4770,4771],{"class":1072},"str",[938,4773,1076],{"class":944},[938,4775,2673],{"class":944},[938,4777,4671],{"class":1727},[938,4779,2673],{"class":944},[938,4781,1085],{"class":944},[938,4783,4784],{"class":2690}," default",[938,4786,1125],{"class":944},[938,4788,2673],{"class":944},[938,4790,4647],{"class":1727},[938,4792,2673],{"class":944},[938,4794,3256],{"class":944},[938,4796,4797,4800,4802,4804,4806,4808,4810,4812,4814,4816,4818,4820,4822,4825],{"class":940,"line":1053},[938,4798,4799],{"class":955},"SIGNNOW_BASIC_AUTH ",[938,4801,1125],{"class":944},[938,4803,4766],{"class":955},[938,4805,1148],{"class":944},[938,4807,4771],{"class":1072},[938,4809,1076],{"class":944},[938,4811,2673],{"class":944},[938,4813,4683],{"class":1727},[938,4815,2673],{"class":944},[938,4817,1085],{"class":944},[938,4819,4784],{"class":2690},[938,4821,1125],{"class":944},[938,4823,4824],{"class":944},"\"\"",[938,4826,3256],{"class":944},[938,4828,4829,4832,4834,4836,4838,4840,4842,4844,4846,4848,4850,4852,4854,4856],{"class":940,"line":1060},[938,4830,4831],{"class":955},"SIGNNOW_USERNAME ",[938,4833,1125],{"class":944},[938,4835,4766],{"class":955},[938,4837,1148],{"class":944},[938,4839,4771],{"class":1072},[938,4841,1076],{"class":944},[938,4843,2673],{"class":944},[938,4845,4693],{"class":1727},[938,4847,2673],{"class":944},[938,4849,1085],{"class":944},[938,4851,4784],{"class":2690},[938,4853,1125],{"class":944},[938,4855,4824],{"class":944},[938,4857,3256],{"class":944},[938,4859,4860,4863,4865,4867,4869,4871,4873,4875,4877,4879,4881,4883,4885,4887],{"class":940,"line":1066},[938,4861,4862],{"class":955},"SIGNNOW_PASSWORD ",[938,4864,1125],{"class":944},[938,4866,4766],{"class":955},[938,4868,1148],{"class":944},[938,4870,4771],{"class":1072},[938,4872,1076],{"class":944},[938,4874,2673],{"class":944},[938,4876,4703],{"class":1727},[938,4878,2673],{"class":944},[938,4880,1085],{"class":944},[938,4882,4784],{"class":2690},[938,4884,1125],{"class":944},[938,4886,4824],{"class":944},[938,4888,3256],{"class":944},[938,4890,4891,4894,4896,4898,4900,4902,4904,4906,4908,4910,4912,4914,4916,4918],{"class":940,"line":1094},[938,4892,4893],{"class":955},"SIGNNOW_WEBHOOK_SECRET ",[938,4895,1125],{"class":944},[938,4897,4766],{"class":955},[938,4899,1148],{"class":944},[938,4901,4771],{"class":1072},[938,4903,1076],{"class":944},[938,4905,2673],{"class":944},[938,4907,4713],{"class":1727},[938,4909,2673],{"class":944},[938,4911,1085],{"class":944},[938,4913,4784],{"class":2690},[938,4915,1125],{"class":944},[938,4917,4824],{"class":944},[938,4919,3256],{"class":944},[938,4921,4922,4925,4927,4929,4931,4933,4935,4937,4939,4941,4943,4945,4947,4949],{"class":940,"line":1116},[938,4923,4924],{"class":955},"SIGNNOW_WEBHOOK_CALLBACK_URL ",[938,4926,1125],{"class":944},[938,4928,4766],{"class":955},[938,4930,1148],{"class":944},[938,4932,4771],{"class":1072},[938,4934,1076],{"class":944},[938,4936,2673],{"class":944},[938,4938,4723],{"class":1727},[938,4940,2673],{"class":944},[938,4942,1085],{"class":944},[938,4944,4784],{"class":2690},[938,4946,1125],{"class":944},[938,4948,4824],{"class":944},[938,4950,3256],{"class":944},[747,4952,4954],{"id":4953},"step-2-build-the-api-client-service-layer","Step 2: Build the API client (service layer)",[752,4956,4957,4958,4960],{},"Rather than scattering HTTP calls throughout the codebase, we encapsulated all SignNow API interactions in a single ",[920,4959,4613],{}," class. This makes testing, error handling, and future changes much simpler.",[774,4962,4964],{"id":4963},"oauth2-authentication-with-token-caching","OAuth2 authentication with token caching",[752,4966,4967],{},"SignNow uses the OAuth2 password grant to obtain access tokens. Tokens expire after a configurable period (typically 1 hour), so we cache them in Redis with a safety buffer:",[929,4969,4972],{"className":2609,"code":4970,"filename":4971,"language":2611,"meta":934,"style":934},"import httpx\nfrom django.conf import settings\nfrom django.core.cache import cache\n\nSIGNNOW_TOKEN_CACHE_KEY = \"signnow_access_token\"\nSIGNNOW_TOKEN_TTL_BUFFER = 300  # Refresh 5 min before expiry\n\n\nclass SignNowService:\n    \"\"\"Raw REST client for the SignNow API using httpx.\"\"\"\n\n    _initialized = False\n    _base_url = \"\"\n    _basic_auth = \"\"\n    _username = \"\"\n    _password = \"\"\n\n    @classmethod\n    def _ensure_initialized(cls) -> bool:\n        \"\"\"Initialize SignNow configuration from settings.\"\"\"\n        if cls._initialized:\n            return True\n\n        cls._base_url = getattr(settings, \"SIGNNOW_API_BASE_URL\", \"\")\n        cls._basic_auth = getattr(settings, \"SIGNNOW_BASIC_AUTH\", \"\")\n        cls._username = getattr(settings, \"SIGNNOW_USERNAME\", \"\")\n        cls._password = getattr(settings, \"SIGNNOW_PASSWORD\", \"\")\n\n        if not all([cls._base_url, cls._basic_auth, cls._username, cls._password]):\n            logger.warning(\"SignNow API not fully configured - missing required settings\")\n            return False\n\n        cls._initialized = True\n        return True\n\n    @classmethod\n    def _get_access_token(cls) -> str | None:\n        \"\"\"Get a valid access token, using cache or requesting a new one.\"\"\"\n        cached = cache.get(SIGNNOW_TOKEN_CACHE_KEY)\n        if cached:\n            return cached\n\n        with httpx.Client(timeout=30.0) as client:\n            resp = client.post(\n                f\"{cls._base_url}/oauth2/token\",\n                data={\n                    \"grant_type\": \"password\",\n                    \"username\": cls._username,\n                    \"password\": cls._password,\n                    \"scope\": \"*\",\n                },\n                headers={\n                    \"Authorization\": f\"Basic {cls._basic_auth}\",\n                    \"Content-Type\": \"application/x-www-form-urlencoded\",\n                },\n            )\n            resp.raise_for_status()\n            data = resp.json()\n\n            token = data[\"access_token\"]\n            expires_in = data.get(\"expires_in\", 3600)\n            ttl = max(expires_in - SIGNNOW_TOKEN_TTL_BUFFER, 60)\n            cache.set(SIGNNOW_TOKEN_CACHE_KEY, token, timeout=ttl)\n\n            return token\n","apps/signnow/services/signnow_service.py",[920,4973,4974,4980,4998,5019,5023,5037,5050,5054,5058,5068,5079,5083,5093,5103,5112,5121,5130,5134,5142,5165,5175,5190,5198,5202,5238,5269,5300,5331,5335,5381,5401,5407,5411,5423,5430,5434,5440,5464,5473,5495,5504,5511,5516,5549,5565,5588,5596,5617,5637,5656,5676,5682,5690,5720,5741,5746,5752,5765,5782,5787,5810,5840,5868,5900,5905],{"__ignoreMap":934},[938,4975,4976,4978],{"class":940,"line":941},[938,4977,2618],{"class":1097},[938,4979,2621],{"class":955},[938,4981,4982,4985,4988,4990,4993,4995],{"class":940,"line":967},[938,4983,4984],{"class":1097},"from",[938,4986,4987],{"class":955}," django",[938,4989,1148],{"class":944},[938,4991,4992],{"class":955},"conf ",[938,4994,2618],{"class":1097},[938,4996,4997],{"class":955}," settings\n",[938,4999,5000,5002,5004,5006,5009,5011,5014,5016],{"class":940,"line":1053},[938,5001,4984],{"class":1097},[938,5003,4987],{"class":955},[938,5005,1148],{"class":944},[938,5007,5008],{"class":955},"core",[938,5010,1148],{"class":944},[938,5012,5013],{"class":955},"cache ",[938,5015,2618],{"class":1097},[938,5017,5018],{"class":955}," cache\n",[938,5020,5021],{"class":940,"line":1060},[938,5022,1057],{"emptyLinePlaceholder":1056},[938,5024,5025,5028,5030,5032,5035],{"class":940,"line":1066},[938,5026,5027],{"class":955},"SIGNNOW_TOKEN_CACHE_KEY ",[938,5029,1125],{"class":944},[938,5031,2635],{"class":944},[938,5033,5034],{"class":1727},"signnow_access_token",[938,5036,2641],{"class":944},[938,5038,5039,5042,5044,5047],{"class":940,"line":1094},[938,5040,5041],{"class":955},"SIGNNOW_TOKEN_TTL_BUFFER ",[938,5043,1125],{"class":944},[938,5045,5046],{"class":1522}," 300",[938,5048,5049],{"class":1535},"  # Refresh 5 min before expiry\n",[938,5051,5052],{"class":940,"line":1116},[938,5053,1057],{"emptyLinePlaceholder":1056},[938,5055,5056],{"class":940,"line":1134},[938,5057,1057],{"emptyLinePlaceholder":1056},[938,5059,5060,5062,5065],{"class":940,"line":1154},[938,5061,1017],{"class":1035},[938,5063,5064],{"class":1020}," SignNowService",[938,5066,5067],{"class":944},":\n",[938,5069,5070,5073,5076],{"class":940,"line":1171},[938,5071,5072],{"class":1097},"    \"\"\"",[938,5074,5075],{"class":1535},"Raw REST client for the SignNow API using httpx.",[938,5077,5078],{"class":1097},"\"\"\"\n",[938,5080,5081],{"class":940,"line":1191},[938,5082,1057],{"emptyLinePlaceholder":1056},[938,5084,5085,5088,5090],{"class":940,"line":1196},[938,5086,5087],{"class":955},"    _initialized ",[938,5089,1125],{"class":944},[938,5091,5092],{"class":944}," False\n",[938,5094,5095,5098,5100],{"class":940,"line":1231},[938,5096,5097],{"class":955},"    _base_url ",[938,5099,1125],{"class":944},[938,5101,5102],{"class":944}," \"\"\n",[938,5104,5105,5108,5110],{"class":940,"line":1237},[938,5106,5107],{"class":955},"    _basic_auth ",[938,5109,1125],{"class":944},[938,5111,5102],{"class":944},[938,5113,5114,5117,5119],{"class":940,"line":1243},[938,5115,5116],{"class":955},"    _username ",[938,5118,1125],{"class":944},[938,5120,5102],{"class":944},[938,5122,5123,5126,5128],{"class":940,"line":2431},[938,5124,5125],{"class":955},"    _password ",[938,5127,1125],{"class":944},[938,5129,5102],{"class":944},[938,5131,5132],{"class":940,"line":2437},[938,5133,1057],{"emptyLinePlaceholder":1056},[938,5135,5136,5139],{"class":940,"line":2443},[938,5137,5138],{"class":944},"    @",[938,5140,5141],{"class":1020},"classmethod\n",[938,5143,5144,5147,5150,5152,5155,5157,5160,5163],{"class":940,"line":2449},[938,5145,5146],{"class":1035},"    def",[938,5148,5149],{"class":1072}," _ensure_initialized",[938,5151,1076],{"class":944},[938,5153,5154],{"class":2690},"cls",[938,5156,1731],{"class":944},[938,5158,5159],{"class":944}," ->",[938,5161,5162],{"class":1020}," bool",[938,5164,5067],{"class":944},[938,5166,5167,5170,5173],{"class":940,"line":2454},[938,5168,5169],{"class":1097},"        \"\"\"",[938,5171,5172],{"class":1535},"Initialize SignNow configuration from settings.",[938,5174,5078],{"class":1097},[938,5176,5177,5180,5183,5185,5188],{"class":940,"line":2460},[938,5178,5179],{"class":1097},"        if",[938,5181,5182],{"class":955}," cls",[938,5184,1148],{"class":944},[938,5186,5187],{"class":948},"_initialized",[938,5189,5067],{"class":944},[938,5191,5192,5195],{"class":940,"line":2466},[938,5193,5194],{"class":1097},"            return",[938,5196,5197],{"class":944}," True\n",[938,5199,5200],{"class":940,"line":2472},[938,5201,1057],{"emptyLinePlaceholder":1056},[938,5203,5204,5207,5209,5212,5215,5218,5220,5223,5225,5227,5229,5231,5233,5236],{"class":940,"line":3049},[938,5205,5206],{"class":955},"        cls",[938,5208,1148],{"class":944},[938,5210,5211],{"class":948},"_base_url",[938,5213,5214],{"class":944}," =",[938,5216,5217],{"class":1072}," getattr",[938,5219,1076],{"class":944},[938,5221,5222],{"class":1072},"settings",[938,5224,1085],{"class":944},[938,5226,2635],{"class":944},[938,5228,4671],{"class":1727},[938,5230,2673],{"class":944},[938,5232,1085],{"class":944},[938,5234,5235],{"class":944}," \"\"",[938,5237,3256],{"class":944},[938,5239,5240,5242,5244,5247,5249,5251,5253,5255,5257,5259,5261,5263,5265,5267],{"class":940,"line":3071},[938,5241,5206],{"class":955},[938,5243,1148],{"class":944},[938,5245,5246],{"class":948},"_basic_auth",[938,5248,5214],{"class":944},[938,5250,5217],{"class":1072},[938,5252,1076],{"class":944},[938,5254,5222],{"class":1072},[938,5256,1085],{"class":944},[938,5258,2635],{"class":944},[938,5260,4683],{"class":1727},[938,5262,2673],{"class":944},[938,5264,1085],{"class":944},[938,5266,5235],{"class":944},[938,5268,3256],{"class":944},[938,5270,5271,5273,5275,5278,5280,5282,5284,5286,5288,5290,5292,5294,5296,5298],{"class":940,"line":3149},[938,5272,5206],{"class":955},[938,5274,1148],{"class":944},[938,5276,5277],{"class":948},"_username",[938,5279,5214],{"class":944},[938,5281,5217],{"class":1072},[938,5283,1076],{"class":944},[938,5285,5222],{"class":1072},[938,5287,1085],{"class":944},[938,5289,2635],{"class":944},[938,5291,4693],{"class":1727},[938,5293,2673],{"class":944},[938,5295,1085],{"class":944},[938,5297,5235],{"class":944},[938,5299,3256],{"class":944},[938,5301,5302,5304,5306,5309,5311,5313,5315,5317,5319,5321,5323,5325,5327,5329],{"class":940,"line":3154},[938,5303,5206],{"class":955},[938,5305,1148],{"class":944},[938,5307,5308],{"class":948},"_password",[938,5310,5214],{"class":944},[938,5312,5217],{"class":1072},[938,5314,1076],{"class":944},[938,5316,5222],{"class":1072},[938,5318,1085],{"class":944},[938,5320,2635],{"class":944},[938,5322,4703],{"class":1727},[938,5324,2673],{"class":944},[938,5326,1085],{"class":944},[938,5328,5235],{"class":944},[938,5330,3256],{"class":944},[938,5332,5333],{"class":940,"line":3159},[938,5334,1057],{"emptyLinePlaceholder":1056},[938,5336,5337,5339,5342,5345,5348,5350,5352,5354,5356,5358,5360,5362,5364,5366,5368,5370,5372,5374,5376,5378],{"class":940,"line":3185},[938,5338,5179],{"class":1097},[938,5340,5341],{"class":944}," not",[938,5343,5344],{"class":1072}," all",[938,5346,5347],{"class":944},"([",[938,5349,5154],{"class":955},[938,5351,1148],{"class":944},[938,5353,5211],{"class":948},[938,5355,1085],{"class":944},[938,5357,5182],{"class":955},[938,5359,1148],{"class":944},[938,5361,5246],{"class":948},[938,5363,1085],{"class":944},[938,5365,5182],{"class":955},[938,5367,1148],{"class":944},[938,5369,5277],{"class":948},[938,5371,1085],{"class":944},[938,5373,5182],{"class":955},[938,5375,1148],{"class":944},[938,5377,5308],{"class":948},[938,5379,5380],{"class":944},"]):\n",[938,5382,5383,5386,5388,5390,5392,5394,5397,5399],{"class":940,"line":3190},[938,5384,5385],{"class":955},"            logger",[938,5387,1148],{"class":944},[938,5389,2204],{"class":1072},[938,5391,1076],{"class":944},[938,5393,2673],{"class":944},[938,5395,5396],{"class":1727},"SignNow API not fully configured - missing required settings",[938,5398,2673],{"class":944},[938,5400,3256],{"class":944},[938,5402,5403,5405],{"class":940,"line":3196},[938,5404,5194],{"class":1097},[938,5406,5092],{"class":944},[938,5408,5409],{"class":940,"line":3213},[938,5410,1057],{"emptyLinePlaceholder":1056},[938,5412,5413,5415,5417,5419,5421],{"class":940,"line":3242},[938,5414,5206],{"class":955},[938,5416,1148],{"class":944},[938,5418,5187],{"class":948},[938,5420,5214],{"class":944},[938,5422,5197],{"class":944},[938,5424,5425,5428],{"class":940,"line":3253},[938,5426,5427],{"class":1097},"        return",[938,5429,5197],{"class":944},[938,5431,5432],{"class":940,"line":3259},[938,5433,1057],{"emptyLinePlaceholder":1056},[938,5435,5436,5438],{"class":940,"line":3264},[938,5437,5138],{"class":944},[938,5439,5141],{"class":1020},[938,5441,5442,5444,5447,5449,5451,5453,5455,5458,5461],{"class":940,"line":3270},[938,5443,5146],{"class":1035},[938,5445,5446],{"class":1072}," _get_access_token",[938,5448,1076],{"class":944},[938,5450,5154],{"class":2690},[938,5452,1731],{"class":944},[938,5454,5159],{"class":944},[938,5456,5457],{"class":1020}," str",[938,5459,5460],{"class":944}," |",[938,5462,5463],{"class":944}," None:\n",[938,5465,5466,5468,5471],{"class":940,"line":3294},[938,5467,5169],{"class":1097},[938,5469,5470],{"class":1535},"Get a valid access token, using cache or requesting a new one.",[938,5472,5078],{"class":1097},[938,5474,5475,5478,5480,5483,5485,5488,5490,5493],{"class":940,"line":3321},[938,5476,5477],{"class":955},"        cached ",[938,5479,1125],{"class":944},[938,5481,5482],{"class":955}," cache",[938,5484,1148],{"class":944},[938,5486,5487],{"class":1072},"get",[938,5489,1076],{"class":944},[938,5491,5492],{"class":1072},"SIGNNOW_TOKEN_CACHE_KEY",[938,5494,3256],{"class":944},[938,5496,5497,5499,5502],{"class":940,"line":3333},[938,5498,5179],{"class":1097},[938,5500,5501],{"class":955}," cached",[938,5503,5067],{"class":944},[938,5505,5506,5508],{"class":940,"line":3356},[938,5507,5194],{"class":1097},[938,5509,5510],{"class":955}," cached\n",[938,5512,5514],{"class":940,"line":5513},42,[938,5515,1057],{"emptyLinePlaceholder":1056},[938,5517,5519,5522,5524,5526,5529,5531,5534,5536,5539,5541,5544,5547],{"class":940,"line":5518},43,[938,5520,5521],{"class":1097},"        with",[938,5523,2660],{"class":955},[938,5525,1148],{"class":944},[938,5527,5528],{"class":1072},"Client",[938,5530,1076],{"class":944},[938,5532,5533],{"class":2690},"timeout",[938,5535,1125],{"class":944},[938,5537,5538],{"class":1522},"30.0",[938,5540,1731],{"class":944},[938,5542,5543],{"class":1097}," as",[938,5545,5546],{"class":955}," client",[938,5548,5067],{"class":944},[938,5550,5552,5555,5557,5559,5561,5563],{"class":940,"line":5551},44,[938,5553,5554],{"class":955},"            resp ",[938,5556,1125],{"class":944},[938,5558,5546],{"class":955},[938,5560,1148],{"class":944},[938,5562,2665],{"class":1072},[938,5564,3210],{"class":944},[938,5566,5568,5571,5573,5575,5577,5579,5581,5583,5586],{"class":940,"line":5567},45,[938,5569,5570],{"class":1035},"                f",[938,5572,2673],{"class":1727},[938,5574,2676],{"class":1522},[938,5576,5154],{"class":955},[938,5578,1148],{"class":944},[938,5580,5211],{"class":948},[938,5582,2682],{"class":1522},[938,5584,5585],{"class":1727},"/oauth2/token\"",[938,5587,2716],{"class":944},[938,5589,5591,5594],{"class":940,"line":5590},46,[938,5592,5593],{"class":2690},"                data",[938,5595,2694],{"class":944},[938,5597,5599,5602,5605,5607,5609,5611,5613,5615],{"class":940,"line":5598},47,[938,5600,5601],{"class":944},"                    \"",[938,5603,5604],{"class":1727},"grant_type",[938,5606,2673],{"class":944},[938,5608,1880],{"class":944},[938,5610,2635],{"class":944},[938,5612,2723],{"class":1727},[938,5614,2673],{"class":944},[938,5616,2716],{"class":944},[938,5618,5620,5622,5625,5627,5629,5631,5633,5635],{"class":940,"line":5619},48,[938,5621,5601],{"class":944},[938,5623,5624],{"class":1727},"username",[938,5626,2673],{"class":944},[938,5628,1880],{"class":944},[938,5630,5182],{"class":955},[938,5632,1148],{"class":944},[938,5634,5277],{"class":948},[938,5636,2716],{"class":944},[938,5638,5640,5642,5644,5646,5648,5650,5652,5654],{"class":940,"line":5639},49,[938,5641,5601],{"class":944},[938,5643,2723],{"class":1727},[938,5645,2673],{"class":944},[938,5647,1880],{"class":944},[938,5649,5182],{"class":955},[938,5651,1148],{"class":944},[938,5653,5308],{"class":948},[938,5655,2716],{"class":944},[938,5657,5659,5661,5664,5666,5668,5670,5672,5674],{"class":940,"line":5658},50,[938,5660,5601],{"class":944},[938,5662,5663],{"class":1727},"scope",[938,5665,2673],{"class":944},[938,5667,1880],{"class":944},[938,5669,2635],{"class":944},[938,5671,1358],{"class":1727},[938,5673,2673],{"class":944},[938,5675,2716],{"class":944},[938,5677,5679],{"class":940,"line":5678},51,[938,5680,5681],{"class":944},"                },\n",[938,5683,5685,5688],{"class":940,"line":5684},52,[938,5686,5687],{"class":2690},"                headers",[938,5689,2694],{"class":944},[938,5691,5693,5695,5697,5699,5701,5703,5706,5708,5710,5712,5714,5716,5718],{"class":940,"line":5692},53,[938,5694,5601],{"class":944},[938,5696,2782],{"class":1727},[938,5698,2673],{"class":944},[938,5700,1880],{"class":944},[938,5702,2789],{"class":1035},[938,5704,5705],{"class":1727},"\"Basic ",[938,5707,2676],{"class":1522},[938,5709,5154],{"class":955},[938,5711,1148],{"class":944},[938,5713,5246],{"class":948},[938,5715,2682],{"class":1522},[938,5717,2673],{"class":1727},[938,5719,2716],{"class":944},[938,5721,5723,5725,5728,5730,5732,5734,5737,5739],{"class":940,"line":5722},54,[938,5724,5601],{"class":944},[938,5726,5727],{"class":1727},"Content-Type",[938,5729,2673],{"class":944},[938,5731,1880],{"class":944},[938,5733,2635],{"class":944},[938,5735,5736],{"class":1727},"application/x-www-form-urlencoded",[938,5738,2673],{"class":944},[938,5740,2716],{"class":944},[938,5742,5744],{"class":940,"line":5743},55,[938,5745,5681],{"class":944},[938,5747,5749],{"class":940,"line":5748},56,[938,5750,5751],{"class":944},"            )\n",[938,5753,5755,5758,5760,5763],{"class":940,"line":5754},57,[938,5756,5757],{"class":955},"            resp",[938,5759,1148],{"class":944},[938,5761,5762],{"class":1072},"raise_for_status",[938,5764,1131],{"class":944},[938,5766,5768,5771,5773,5776,5778,5780],{"class":940,"line":5767},58,[938,5769,5770],{"class":955},"            data ",[938,5772,1125],{"class":944},[938,5774,5775],{"class":955}," resp",[938,5777,1148],{"class":944},[938,5779,2754],{"class":1072},[938,5781,1131],{"class":944},[938,5783,5785],{"class":940,"line":5784},59,[938,5786,1057],{"emptyLinePlaceholder":1056},[938,5788,5790,5793,5795,5798,5801,5803,5806,5808],{"class":940,"line":5789},60,[938,5791,5792],{"class":955},"            token ",[938,5794,1125],{"class":944},[938,5796,5797],{"class":955}," data",[938,5799,5800],{"class":944},"[",[938,5802,2673],{"class":944},[938,5804,5805],{"class":1727},"access_token",[938,5807,2673],{"class":944},[938,5809,2767],{"class":944},[938,5811,5813,5816,5818,5820,5822,5824,5826,5828,5831,5833,5835,5838],{"class":940,"line":5812},61,[938,5814,5815],{"class":955},"            expires_in ",[938,5817,1125],{"class":944},[938,5819,5797],{"class":955},[938,5821,1148],{"class":944},[938,5823,5487],{"class":1072},[938,5825,1076],{"class":944},[938,5827,2673],{"class":944},[938,5829,5830],{"class":1727},"expires_in",[938,5832,2673],{"class":944},[938,5834,1085],{"class":944},[938,5836,5837],{"class":1522}," 3600",[938,5839,3256],{"class":944},[938,5841,5843,5846,5848,5851,5853,5856,5858,5861,5863,5866],{"class":940,"line":5842},62,[938,5844,5845],{"class":955},"            ttl ",[938,5847,1125],{"class":944},[938,5849,5850],{"class":1072}," max",[938,5852,1076],{"class":944},[938,5854,5855],{"class":1072},"expires_in ",[938,5857,1312],{"class":944},[938,5859,5860],{"class":1072}," SIGNNOW_TOKEN_TTL_BUFFER",[938,5862,1085],{"class":944},[938,5864,5865],{"class":1522}," 60",[938,5867,3256],{"class":944},[938,5869,5871,5874,5876,5879,5881,5883,5885,5888,5890,5893,5895,5898],{"class":940,"line":5870},63,[938,5872,5873],{"class":955},"            cache",[938,5875,1148],{"class":944},[938,5877,5878],{"class":1072},"set",[938,5880,1076],{"class":944},[938,5882,5492],{"class":1072},[938,5884,1085],{"class":944},[938,5886,5887],{"class":1072}," token",[938,5889,1085],{"class":944},[938,5891,5892],{"class":2690}," timeout",[938,5894,1125],{"class":944},[938,5896,5897],{"class":1072},"ttl",[938,5899,3256],{"class":944},[938,5901,5903],{"class":940,"line":5902},64,[938,5904,1057],{"emptyLinePlaceholder":1056},[938,5906,5908,5910],{"class":940,"line":5907},65,[938,5909,5194],{"class":1097},[938,5911,5912],{"class":955}," token\n",[2330,5914,5915],{},[752,5916,5917,5918,5923,5924,5927,5928,5930,5931,5934],{},"We use ",[2036,5919,5922],{"href":5920,"rel":5921},"https://www.python-httpx.org/",[2040],"httpx"," instead of ",[920,5925,5926],{},"requests"," for its modern API, built-in timeout support, and async capabilities. If you later need to make concurrent API calls, ",[920,5929,5922],{}," supports ",[920,5932,5933],{},"AsyncClient"," out of the box.",[774,5936,5938],{"id":5937},"document-operations","Document operations",[752,5940,5941],{},"With authentication handled, the core API methods are straightforward:",[929,5943,5945],{"className":2609,"code":5944,"filename":4971,"language":2611,"meta":934,"style":934},"@classmethod\ndef upload_document(cls, pdf_bytes: bytes, filename: str) -> dict | None:\n    \"\"\"Upload a PDF to SignNow. Returns {\"id\": \"document_id\"} on success.\"\"\"\n    token = cls._get_access_token()\n    if not token:\n        return None\n\n    with httpx.Client(timeout=60.0) as client:\n        resp = client.post(\n            f\"{cls._base_url}/document\",\n            headers=cls._auth_headers(token),\n            files={\"file\": (filename, pdf_bytes, \"application/pdf\")},\n        )\n        resp.raise_for_status()\n        return {\"id\": resp.json().get(\"id\", \"\")}\n\n@classmethod\ndef add_signature_fields(cls, document_id: str, fields: list[dict]) -> bool:\n    \"\"\"Add signature/text fields to an uploaded document.\"\"\"\n    token = cls._get_access_token()\n    if not token:\n        return False\n\n    with httpx.Client(timeout=30.0) as client:\n        resp = client.put(\n            f\"{cls._base_url}/document/{document_id}\",\n            headers={**cls._auth_headers(token), \"Content-Type\": \"application/json\"},\n            json={\"fields\": fields},\n        )\n        resp.raise_for_status()\n        return True\n\n@classmethod\ndef download_signed_document(cls, document_id: str) -> bytes | None:\n    \"\"\"Download the signed (collapsed) PDF.\"\"\"\n    token = cls._get_access_token()\n    if not token:\n        return None\n\n    with httpx.Client(timeout=60.0) as client:\n        resp = client.get(\n            f\"{cls._base_url}/document/{document_id}/download\",\n            headers=cls._auth_headers(token),\n            params={\"type\": \"collapsed\"},\n        )\n        resp.raise_for_status()\n        return resp.content\n",[920,5946,5947,5954,5996,6005,6021,6032,6039,6043,6071,6086,6108,6129,6166,6171,6182,6222,6226,6232,6276,6285,6299,6309,6315,6319,6345,6360,6390,6427,6447,6451,6461,6467,6471,6477,6506,6515,6529,6539,6545,6549,6575,6589,6618,6636,6661,6665,6675],{"__ignoreMap":934},[938,5948,5949,5952],{"class":940,"line":941},[938,5950,5951],{"class":944},"@",[938,5953,5141],{"class":1020},[938,5955,5956,5959,5962,5964,5966,5968,5971,5973,5976,5978,5981,5983,5985,5987,5989,5992,5994],{"class":940,"line":967},[938,5957,5958],{"class":1035},"def",[938,5960,5961],{"class":1072}," upload_document",[938,5963,1076],{"class":944},[938,5965,5154],{"class":2690},[938,5967,1085],{"class":944},[938,5969,5970],{"class":2690}," pdf_bytes",[938,5972,1880],{"class":944},[938,5974,5975],{"class":1020}," bytes",[938,5977,1085],{"class":944},[938,5979,5980],{"class":2690}," filename",[938,5982,1880],{"class":944},[938,5984,5457],{"class":1020},[938,5986,1731],{"class":944},[938,5988,5159],{"class":944},[938,5990,5991],{"class":1020}," dict",[938,5993,5460],{"class":944},[938,5995,5463],{"class":944},[938,5997,5998,6000,6003],{"class":940,"line":1053},[938,5999,5072],{"class":1097},[938,6001,6002],{"class":1535},"Upload a PDF to SignNow. Returns {\"id\": \"document_id\"} on success.",[938,6004,5078],{"class":1097},[938,6006,6007,6010,6012,6014,6016,6019],{"class":940,"line":1060},[938,6008,6009],{"class":955},"    token ",[938,6011,1125],{"class":944},[938,6013,5182],{"class":955},[938,6015,1148],{"class":944},[938,6017,6018],{"class":1072},"_get_access_token",[938,6020,1131],{"class":944},[938,6022,6023,6026,6028,6030],{"class":940,"line":1066},[938,6024,6025],{"class":1097},"    if",[938,6027,5341],{"class":944},[938,6029,5887],{"class":955},[938,6031,5067],{"class":944},[938,6033,6034,6036],{"class":940,"line":1094},[938,6035,5427],{"class":1097},[938,6037,6038],{"class":944}," None\n",[938,6040,6041],{"class":940,"line":1116},[938,6042,1057],{"emptyLinePlaceholder":1056},[938,6044,6045,6048,6050,6052,6054,6056,6058,6060,6063,6065,6067,6069],{"class":940,"line":1134},[938,6046,6047],{"class":1097},"    with",[938,6049,2660],{"class":955},[938,6051,1148],{"class":944},[938,6053,5528],{"class":1072},[938,6055,1076],{"class":944},[938,6057,5533],{"class":2690},[938,6059,1125],{"class":944},[938,6061,6062],{"class":1522},"60.0",[938,6064,1731],{"class":944},[938,6066,5543],{"class":1097},[938,6068,5546],{"class":955},[938,6070,5067],{"class":944},[938,6072,6073,6076,6078,6080,6082,6084],{"class":940,"line":1154},[938,6074,6075],{"class":955},"        resp ",[938,6077,1125],{"class":944},[938,6079,5546],{"class":955},[938,6081,1148],{"class":944},[938,6083,2665],{"class":1072},[938,6085,3210],{"class":944},[938,6087,6088,6091,6093,6095,6097,6099,6101,6103,6106],{"class":940,"line":1171},[938,6089,6090],{"class":1035},"            f",[938,6092,2673],{"class":1727},[938,6094,2676],{"class":1522},[938,6096,5154],{"class":955},[938,6098,1148],{"class":944},[938,6100,5211],{"class":948},[938,6102,2682],{"class":1522},[938,6104,6105],{"class":1727},"/document\"",[938,6107,2716],{"class":944},[938,6109,6110,6113,6115,6117,6119,6122,6124,6126],{"class":940,"line":1191},[938,6111,6112],{"class":2690},"            headers",[938,6114,1125],{"class":944},[938,6116,5154],{"class":955},[938,6118,1148],{"class":944},[938,6120,6121],{"class":1072},"_auth_headers",[938,6123,1076],{"class":944},[938,6125,2762],{"class":1072},[938,6127,6128],{"class":944},"),\n",[938,6130,6131,6134,6136,6138,6141,6143,6145,6147,6150,6152,6154,6156,6158,6161,6163],{"class":940,"line":1196},[938,6132,6133],{"class":2690},"            files",[938,6135,3339],{"class":944},[938,6137,2673],{"class":944},[938,6139,6140],{"class":1727},"file",[938,6142,2673],{"class":944},[938,6144,1880],{"class":944},[938,6146,1101],{"class":944},[938,6148,6149],{"class":1072},"filename",[938,6151,1085],{"class":944},[938,6153,5970],{"class":1072},[938,6155,1085],{"class":944},[938,6157,2635],{"class":944},[938,6159,6160],{"class":1727},"application/pdf",[938,6162,2673],{"class":944},[938,6164,6165],{"class":944},")},\n",[938,6167,6168],{"class":940,"line":1231},[938,6169,6170],{"class":944},"        )\n",[938,6172,6173,6176,6178,6180],{"class":940,"line":1237},[938,6174,6175],{"class":955},"        resp",[938,6177,1148],{"class":944},[938,6179,5762],{"class":1072},[938,6181,1131],{"class":944},[938,6183,6184,6186,6188,6190,6192,6194,6196,6198,6200,6202,6205,6207,6209,6211,6213,6215,6217,6219],{"class":940,"line":1243},[938,6185,5427],{"class":1097},[938,6187,2777],{"class":944},[938,6189,2673],{"class":944},[938,6191,3178],{"class":1727},[938,6193,2673],{"class":944},[938,6195,1880],{"class":944},[938,6197,5775],{"class":955},[938,6199,1148],{"class":944},[938,6201,2754],{"class":1072},[938,6203,6204],{"class":944},"().",[938,6206,5487],{"class":1072},[938,6208,1076],{"class":944},[938,6210,2673],{"class":944},[938,6212,3178],{"class":1727},[938,6214,2673],{"class":944},[938,6216,1085],{"class":944},[938,6218,5235],{"class":944},[938,6220,6221],{"class":944},")}\n",[938,6223,6224],{"class":940,"line":2431},[938,6225,1057],{"emptyLinePlaceholder":1056},[938,6227,6228,6230],{"class":940,"line":2437},[938,6229,5951],{"class":944},[938,6231,5141],{"class":1020},[938,6233,6234,6236,6239,6241,6243,6245,6248,6250,6252,6254,6257,6259,6262,6264,6267,6270,6272,6274],{"class":940,"line":2443},[938,6235,5958],{"class":1035},[938,6237,6238],{"class":1072}," add_signature_fields",[938,6240,1076],{"class":944},[938,6242,5154],{"class":2690},[938,6244,1085],{"class":944},[938,6246,6247],{"class":2690}," document_id",[938,6249,1880],{"class":944},[938,6251,5457],{"class":1020},[938,6253,1085],{"class":944},[938,6255,6256],{"class":2690}," fields",[938,6258,1880],{"class":944},[938,6260,6261],{"class":955}," list",[938,6263,5800],{"class":944},[938,6265,6266],{"class":1020},"dict",[938,6268,6269],{"class":944},"])",[938,6271,5159],{"class":944},[938,6273,5162],{"class":1020},[938,6275,5067],{"class":944},[938,6277,6278,6280,6283],{"class":940,"line":2449},[938,6279,5072],{"class":1097},[938,6281,6282],{"class":1535},"Add signature/text fields to an uploaded document.",[938,6284,5078],{"class":1097},[938,6286,6287,6289,6291,6293,6295,6297],{"class":940,"line":2454},[938,6288,6009],{"class":955},[938,6290,1125],{"class":944},[938,6292,5182],{"class":955},[938,6294,1148],{"class":944},[938,6296,6018],{"class":1072},[938,6298,1131],{"class":944},[938,6300,6301,6303,6305,6307],{"class":940,"line":2460},[938,6302,6025],{"class":1097},[938,6304,5341],{"class":944},[938,6306,5887],{"class":955},[938,6308,5067],{"class":944},[938,6310,6311,6313],{"class":940,"line":2466},[938,6312,5427],{"class":1097},[938,6314,5092],{"class":944},[938,6316,6317],{"class":940,"line":2472},[938,6318,1057],{"emptyLinePlaceholder":1056},[938,6320,6321,6323,6325,6327,6329,6331,6333,6335,6337,6339,6341,6343],{"class":940,"line":3049},[938,6322,6047],{"class":1097},[938,6324,2660],{"class":955},[938,6326,1148],{"class":944},[938,6328,5528],{"class":1072},[938,6330,1076],{"class":944},[938,6332,5533],{"class":2690},[938,6334,1125],{"class":944},[938,6336,5538],{"class":1522},[938,6338,1731],{"class":944},[938,6340,5543],{"class":1097},[938,6342,5546],{"class":955},[938,6344,5067],{"class":944},[938,6346,6347,6349,6351,6353,6355,6358],{"class":940,"line":3071},[938,6348,6075],{"class":955},[938,6350,1125],{"class":944},[938,6352,5546],{"class":955},[938,6354,1148],{"class":944},[938,6356,6357],{"class":1072},"put",[938,6359,3210],{"class":944},[938,6361,6362,6364,6366,6368,6370,6372,6374,6376,6379,6381,6384,6386,6388],{"class":940,"line":3149},[938,6363,6090],{"class":1035},[938,6365,2673],{"class":1727},[938,6367,2676],{"class":1522},[938,6369,5154],{"class":955},[938,6371,1148],{"class":944},[938,6373,5211],{"class":948},[938,6375,2682],{"class":1522},[938,6377,6378],{"class":1727},"/document/",[938,6380,2676],{"class":1522},[938,6382,6383],{"class":1072},"document_id",[938,6385,2682],{"class":1522},[938,6387,2673],{"class":1727},[938,6389,2716],{"class":944},[938,6391,6392,6394,6397,6399,6401,6403,6405,6407,6410,6412,6414,6416,6418,6420,6423,6425],{"class":940,"line":3154},[938,6393,6112],{"class":2690},[938,6395,6396],{"class":944},"={**",[938,6398,5154],{"class":955},[938,6400,1148],{"class":944},[938,6402,6121],{"class":1072},[938,6404,1076],{"class":944},[938,6406,2762],{"class":1072},[938,6408,6409],{"class":944},"),",[938,6411,2635],{"class":944},[938,6413,5727],{"class":1727},[938,6415,2673],{"class":944},[938,6417,1880],{"class":944},[938,6419,2635],{"class":944},[938,6421,6422],{"class":1727},"application/json",[938,6424,2673],{"class":944},[938,6426,2951],{"class":944},[938,6428,6429,6432,6434,6436,6439,6441,6443,6445],{"class":940,"line":3159},[938,6430,6431],{"class":2690},"            json",[938,6433,3339],{"class":944},[938,6435,2673],{"class":944},[938,6437,6438],{"class":1727},"fields",[938,6440,2673],{"class":944},[938,6442,1880],{"class":944},[938,6444,6256],{"class":1072},[938,6446,2951],{"class":944},[938,6448,6449],{"class":940,"line":3185},[938,6450,6170],{"class":944},[938,6452,6453,6455,6457,6459],{"class":940,"line":3190},[938,6454,6175],{"class":955},[938,6456,1148],{"class":944},[938,6458,5762],{"class":1072},[938,6460,1131],{"class":944},[938,6462,6463,6465],{"class":940,"line":3196},[938,6464,5427],{"class":1097},[938,6466,5197],{"class":944},[938,6468,6469],{"class":940,"line":3213},[938,6470,1057],{"emptyLinePlaceholder":1056},[938,6472,6473,6475],{"class":940,"line":3242},[938,6474,5951],{"class":944},[938,6476,5141],{"class":1020},[938,6478,6479,6481,6484,6486,6488,6490,6492,6494,6496,6498,6500,6502,6504],{"class":940,"line":3253},[938,6480,5958],{"class":1035},[938,6482,6483],{"class":1072}," download_signed_document",[938,6485,1076],{"class":944},[938,6487,5154],{"class":2690},[938,6489,1085],{"class":944},[938,6491,6247],{"class":2690},[938,6493,1880],{"class":944},[938,6495,5457],{"class":1020},[938,6497,1731],{"class":944},[938,6499,5159],{"class":944},[938,6501,5975],{"class":1020},[938,6503,5460],{"class":944},[938,6505,5463],{"class":944},[938,6507,6508,6510,6513],{"class":940,"line":3259},[938,6509,5072],{"class":1097},[938,6511,6512],{"class":1535},"Download the signed (collapsed) PDF.",[938,6514,5078],{"class":1097},[938,6516,6517,6519,6521,6523,6525,6527],{"class":940,"line":3264},[938,6518,6009],{"class":955},[938,6520,1125],{"class":944},[938,6522,5182],{"class":955},[938,6524,1148],{"class":944},[938,6526,6018],{"class":1072},[938,6528,1131],{"class":944},[938,6530,6531,6533,6535,6537],{"class":940,"line":3270},[938,6532,6025],{"class":1097},[938,6534,5341],{"class":944},[938,6536,5887],{"class":955},[938,6538,5067],{"class":944},[938,6540,6541,6543],{"class":940,"line":3294},[938,6542,5427],{"class":1097},[938,6544,6038],{"class":944},[938,6546,6547],{"class":940,"line":3321},[938,6548,1057],{"emptyLinePlaceholder":1056},[938,6550,6551,6553,6555,6557,6559,6561,6563,6565,6567,6569,6571,6573],{"class":940,"line":3333},[938,6552,6047],{"class":1097},[938,6554,2660],{"class":955},[938,6556,1148],{"class":944},[938,6558,5528],{"class":1072},[938,6560,1076],{"class":944},[938,6562,5533],{"class":2690},[938,6564,1125],{"class":944},[938,6566,6062],{"class":1522},[938,6568,1731],{"class":944},[938,6570,5543],{"class":1097},[938,6572,5546],{"class":955},[938,6574,5067],{"class":944},[938,6576,6577,6579,6581,6583,6585,6587],{"class":940,"line":3356},[938,6578,6075],{"class":955},[938,6580,1125],{"class":944},[938,6582,5546],{"class":955},[938,6584,1148],{"class":944},[938,6586,5487],{"class":1072},[938,6588,3210],{"class":944},[938,6590,6591,6593,6595,6597,6599,6601,6603,6605,6607,6609,6611,6613,6616],{"class":940,"line":5513},[938,6592,6090],{"class":1035},[938,6594,2673],{"class":1727},[938,6596,2676],{"class":1522},[938,6598,5154],{"class":955},[938,6600,1148],{"class":944},[938,6602,5211],{"class":948},[938,6604,2682],{"class":1522},[938,6606,6378],{"class":1727},[938,6608,2676],{"class":1522},[938,6610,6383],{"class":1072},[938,6612,2682],{"class":1522},[938,6614,6615],{"class":1727},"/download\"",[938,6617,2716],{"class":944},[938,6619,6620,6622,6624,6626,6628,6630,6632,6634],{"class":940,"line":5518},[938,6621,6112],{"class":2690},[938,6623,1125],{"class":944},[938,6625,5154],{"class":955},[938,6627,1148],{"class":944},[938,6629,6121],{"class":1072},[938,6631,1076],{"class":944},[938,6633,2762],{"class":1072},[938,6635,6128],{"class":944},[938,6637,6638,6641,6643,6645,6648,6650,6652,6654,6657,6659],{"class":940,"line":5551},[938,6639,6640],{"class":2690},"            params",[938,6642,3339],{"class":944},[938,6644,2673],{"class":944},[938,6646,6647],{"class":1727},"type",[938,6649,2673],{"class":944},[938,6651,1880],{"class":944},[938,6653,2635],{"class":944},[938,6655,6656],{"class":1727},"collapsed",[938,6658,2673],{"class":944},[938,6660,2951],{"class":944},[938,6662,6663],{"class":940,"line":5567},[938,6664,6170],{"class":944},[938,6666,6667,6669,6671,6673],{"class":940,"line":5590},[938,6668,6175],{"class":955},[938,6670,1148],{"class":944},[938,6672,5762],{"class":1072},[938,6674,1131],{"class":944},[938,6676,6677,6679,6681,6683],{"class":940,"line":5598},[938,6678,5427],{"class":1097},[938,6680,5775],{"class":955},[938,6682,1148],{"class":944},[938,6684,6685],{"class":948},"content\n",[774,6687,6689],{"id":6688},"sending-invites","Sending invites",[752,6691,6692],{},"SignNow supports two invite types:",[779,6694,6695,6701],{},[782,6696,6697,6700],{},[755,6698,6699],{},"Freeform invites"," - The signer places their signature wherever they want",[782,6702,6703,6706],{},[755,6704,6705],{},"Role-based invites"," - Signature fields are pre-positioned, and signers fill specific roles",[752,6708,6709],{},"We used role-based invites because we wanted to control exactly where the signature appears on each document:",[929,6711,6713],{"className":2609,"code":6712,"filename":4971,"language":2611,"meta":934,"style":934},"@classmethod\ndef send_role_based_invite(\n    cls,\n    document_id: str,\n    signers: list[dict],\n    from_email: str,\n    subject: str = \"\",\n    message: str = \"\",\n) -> dict | None:\n    \"\"\"\n    Send a role-based invite with specific field assignments.\n    Each signer dict: {\"email\": ..., \"role\": ..., \"role_id\": ..., \"order\": ...}\n    \"\"\"\n    token = cls._get_access_token()\n    if not token:\n        return None\n\n    payload = {\"to\": signers, \"from\": from_email}\n    if subject:\n        payload[\"subject\"] = subject\n    if message:\n        payload[\"message\"] = message\n\n    with httpx.Client(timeout=30.0) as client:\n        resp = client.post(\n            f\"{cls._base_url}/document/{document_id}/invite\",\n            headers={**cls._auth_headers(token), \"Content-Type\": \"application/json\"},\n            json=payload,\n        )\n        resp.raise_for_status()\n        return resp.json()\n",[920,6714,6715,6721,6730,6737,6748,6764,6775,6790,6805,6817,6822,6827,6832,6836,6850,6860,6866,6870,6906,6915,6937,6946,6966,6970,6996,7010,7039,7073,7084,7088,7098],{"__ignoreMap":934},[938,6716,6717,6719],{"class":940,"line":941},[938,6718,5951],{"class":944},[938,6720,5141],{"class":1020},[938,6722,6723,6725,6728],{"class":940,"line":967},[938,6724,5958],{"class":1035},[938,6726,6727],{"class":1072}," send_role_based_invite",[938,6729,3210],{"class":944},[938,6731,6732,6735],{"class":940,"line":1053},[938,6733,6734],{"class":2690},"    cls",[938,6736,2716],{"class":944},[938,6738,6739,6742,6744,6746],{"class":940,"line":1060},[938,6740,6741],{"class":2690},"    document_id",[938,6743,1880],{"class":944},[938,6745,5457],{"class":1020},[938,6747,2716],{"class":944},[938,6749,6750,6753,6755,6757,6759,6761],{"class":940,"line":1066},[938,6751,6752],{"class":2690},"    signers",[938,6754,1880],{"class":944},[938,6756,6261],{"class":955},[938,6758,5800],{"class":944},[938,6760,6266],{"class":1020},[938,6762,6763],{"class":944},"],\n",[938,6765,6766,6769,6771,6773],{"class":940,"line":1094},[938,6767,6768],{"class":2690},"    from_email",[938,6770,1880],{"class":944},[938,6772,5457],{"class":1020},[938,6774,2716],{"class":944},[938,6776,6777,6780,6782,6784,6786,6788],{"class":940,"line":1116},[938,6778,6779],{"class":2690},"    subject",[938,6781,1880],{"class":944},[938,6783,5457],{"class":1020},[938,6785,5214],{"class":944},[938,6787,5235],{"class":944},[938,6789,2716],{"class":944},[938,6791,6792,6795,6797,6799,6801,6803],{"class":940,"line":1134},[938,6793,6794],{"class":2690},"    message",[938,6796,1880],{"class":944},[938,6798,5457],{"class":1020},[938,6800,5214],{"class":944},[938,6802,5235],{"class":944},[938,6804,2716],{"class":944},[938,6806,6807,6809,6811,6813,6815],{"class":940,"line":1154},[938,6808,1731],{"class":944},[938,6810,5159],{"class":944},[938,6812,5991],{"class":1020},[938,6814,5460],{"class":944},[938,6816,5463],{"class":944},[938,6818,6819],{"class":940,"line":1171},[938,6820,6821],{"class":1097},"    \"\"\"\n",[938,6823,6824],{"class":940,"line":1191},[938,6825,6826],{"class":1535},"    Send a role-based invite with specific field assignments.\n",[938,6828,6829],{"class":940,"line":1196},[938,6830,6831],{"class":1535},"    Each signer dict: {\"email\": ..., \"role\": ..., \"role_id\": ..., \"order\": ...}\n",[938,6833,6834],{"class":940,"line":1231},[938,6835,6821],{"class":1097},[938,6837,6838,6840,6842,6844,6846,6848],{"class":940,"line":1237},[938,6839,6009],{"class":955},[938,6841,1125],{"class":944},[938,6843,5182],{"class":955},[938,6845,1148],{"class":944},[938,6847,6018],{"class":1072},[938,6849,1131],{"class":944},[938,6851,6852,6854,6856,6858],{"class":940,"line":1243},[938,6853,6025],{"class":1097},[938,6855,5341],{"class":944},[938,6857,5887],{"class":955},[938,6859,5067],{"class":944},[938,6861,6862,6864],{"class":940,"line":2431},[938,6863,5427],{"class":1097},[938,6865,6038],{"class":944},[938,6867,6868],{"class":940,"line":2437},[938,6869,1057],{"emptyLinePlaceholder":1056},[938,6871,6872,6875,6877,6879,6881,6884,6886,6888,6891,6893,6895,6897,6899,6901,6904],{"class":940,"line":2443},[938,6873,6874],{"class":955},"    payload ",[938,6876,1125],{"class":944},[938,6878,2777],{"class":944},[938,6880,2673],{"class":944},[938,6882,6883],{"class":1727},"to",[938,6885,2673],{"class":944},[938,6887,1880],{"class":944},[938,6889,6890],{"class":955}," signers",[938,6892,1085],{"class":944},[938,6894,2635],{"class":944},[938,6896,4984],{"class":1727},[938,6898,2673],{"class":944},[938,6900,1880],{"class":944},[938,6902,6903],{"class":955}," from_email",[938,6905,1246],{"class":944},[938,6907,6908,6910,6913],{"class":940,"line":2449},[938,6909,6025],{"class":1097},[938,6911,6912],{"class":955}," subject",[938,6914,5067],{"class":944},[938,6916,6917,6920,6922,6924,6927,6929,6932,6934],{"class":940,"line":2454},[938,6918,6919],{"class":955},"        payload",[938,6921,5800],{"class":944},[938,6923,2673],{"class":944},[938,6925,6926],{"class":1727},"subject",[938,6928,2673],{"class":944},[938,6930,6931],{"class":944},"]",[938,6933,5214],{"class":944},[938,6935,6936],{"class":955}," subject\n",[938,6938,6939,6941,6944],{"class":940,"line":2460},[938,6940,6025],{"class":1097},[938,6942,6943],{"class":955}," message",[938,6945,5067],{"class":944},[938,6947,6948,6950,6952,6954,6957,6959,6961,6963],{"class":940,"line":2466},[938,6949,6919],{"class":955},[938,6951,5800],{"class":944},[938,6953,2673],{"class":944},[938,6955,6956],{"class":1727},"message",[938,6958,2673],{"class":944},[938,6960,6931],{"class":944},[938,6962,5214],{"class":944},[938,6964,6965],{"class":955}," message\n",[938,6967,6968],{"class":940,"line":2472},[938,6969,1057],{"emptyLinePlaceholder":1056},[938,6971,6972,6974,6976,6978,6980,6982,6984,6986,6988,6990,6992,6994],{"class":940,"line":3049},[938,6973,6047],{"class":1097},[938,6975,2660],{"class":955},[938,6977,1148],{"class":944},[938,6979,5528],{"class":1072},[938,6981,1076],{"class":944},[938,6983,5533],{"class":2690},[938,6985,1125],{"class":944},[938,6987,5538],{"class":1522},[938,6989,1731],{"class":944},[938,6991,5543],{"class":1097},[938,6993,5546],{"class":955},[938,6995,5067],{"class":944},[938,6997,6998,7000,7002,7004,7006,7008],{"class":940,"line":3071},[938,6999,6075],{"class":955},[938,7001,1125],{"class":944},[938,7003,5546],{"class":955},[938,7005,1148],{"class":944},[938,7007,2665],{"class":1072},[938,7009,3210],{"class":944},[938,7011,7012,7014,7016,7018,7020,7022,7024,7026,7028,7030,7032,7034,7037],{"class":940,"line":3149},[938,7013,6090],{"class":1035},[938,7015,2673],{"class":1727},[938,7017,2676],{"class":1522},[938,7019,5154],{"class":955},[938,7021,1148],{"class":944},[938,7023,5211],{"class":948},[938,7025,2682],{"class":1522},[938,7027,6378],{"class":1727},[938,7029,2676],{"class":1522},[938,7031,6383],{"class":1072},[938,7033,2682],{"class":1522},[938,7035,7036],{"class":1727},"/invite\"",[938,7038,2716],{"class":944},[938,7040,7041,7043,7045,7047,7049,7051,7053,7055,7057,7059,7061,7063,7065,7067,7069,7071],{"class":940,"line":3154},[938,7042,6112],{"class":2690},[938,7044,6396],{"class":944},[938,7046,5154],{"class":955},[938,7048,1148],{"class":944},[938,7050,6121],{"class":1072},[938,7052,1076],{"class":944},[938,7054,2762],{"class":1072},[938,7056,6409],{"class":944},[938,7058,2635],{"class":944},[938,7060,5727],{"class":1727},[938,7062,2673],{"class":944},[938,7064,1880],{"class":944},[938,7066,2635],{"class":944},[938,7068,6422],{"class":1727},[938,7070,2673],{"class":944},[938,7072,2951],{"class":944},[938,7074,7075,7077,7079,7082],{"class":940,"line":3159},[938,7076,6431],{"class":2690},[938,7078,1125],{"class":944},[938,7080,7081],{"class":1072},"payload",[938,7083,2716],{"class":944},[938,7085,7086],{"class":940,"line":3185},[938,7087,6170],{"class":944},[938,7089,7090,7092,7094,7096],{"class":940,"line":3190},[938,7091,6175],{"class":955},[938,7093,1148],{"class":944},[938,7095,5762],{"class":1072},[938,7097,1131],{"class":944},[938,7099,7100,7102,7104,7106,7108],{"class":940,"line":3196},[938,7101,5427],{"class":1097},[938,7103,5775],{"class":955},[938,7105,1148],{"class":944},[938,7107,2754],{"class":1072},[938,7109,1131],{"class":944},[774,7111,7113],{"id":7112},"webhook-registration","Webhook registration",[752,7115,7116],{},"To get notified when a document is signed, we register a webhook for each document:",[929,7118,7120],{"className":2609,"code":7119,"filename":4971,"language":2611,"meta":934,"style":934},"@classmethod\ndef register_webhook(cls, event: str, entity_id: str, callback_url: str) -> bool:\n    \"\"\"Register a webhook callback for a specific event on a document.\"\"\"\n    token = cls._get_access_token()\n    if not token:\n        return False\n\n    payload = {\n        \"event\": event,\n        \"entity_id\": entity_id,\n        \"action\": \"callback\",\n        \"attributes\": {\n            \"callback\": callback_url,\n            \"use_tls_12\": True,\n        },\n    }\n\n    webhook_secret = getattr(settings, \"SIGNNOW_WEBHOOK_SECRET\", \"\")\n    if webhook_secret:\n        payload[\"attributes\"][\"secret_key\"] = webhook_secret\n\n    with httpx.Client(timeout=30.0) as client:\n        resp = client.post(\n            f\"{cls._base_url}/v2/events\",\n            headers={**cls._auth_headers(token), \"Content-Type\": \"application/json\"},\n            json=payload,\n        )\n        resp.raise_for_status()\n        return True\n",[920,7121,7122,7128,7174,7183,7197,7207,7213,7217,7225,7240,7255,7275,7288,7303,7317,7322,7326,7330,7357,7366,7395,7399,7425,7439,7460,7494,7504,7508,7518],{"__ignoreMap":934},[938,7123,7124,7126],{"class":940,"line":941},[938,7125,5951],{"class":944},[938,7127,5141],{"class":1020},[938,7129,7130,7132,7135,7137,7139,7141,7144,7146,7148,7150,7153,7155,7157,7159,7162,7164,7166,7168,7170,7172],{"class":940,"line":967},[938,7131,5958],{"class":1035},[938,7133,7134],{"class":1072}," register_webhook",[938,7136,1076],{"class":944},[938,7138,5154],{"class":2690},[938,7140,1085],{"class":944},[938,7142,7143],{"class":2690}," event",[938,7145,1880],{"class":944},[938,7147,5457],{"class":1020},[938,7149,1085],{"class":944},[938,7151,7152],{"class":2690}," entity_id",[938,7154,1880],{"class":944},[938,7156,5457],{"class":1020},[938,7158,1085],{"class":944},[938,7160,7161],{"class":2690}," callback_url",[938,7163,1880],{"class":944},[938,7165,5457],{"class":1020},[938,7167,1731],{"class":944},[938,7169,5159],{"class":944},[938,7171,5162],{"class":1020},[938,7173,5067],{"class":944},[938,7175,7176,7178,7181],{"class":940,"line":1053},[938,7177,5072],{"class":1097},[938,7179,7180],{"class":1535},"Register a webhook callback for a specific event on a document.",[938,7182,5078],{"class":1097},[938,7184,7185,7187,7189,7191,7193,7195],{"class":940,"line":1060},[938,7186,6009],{"class":955},[938,7188,1125],{"class":944},[938,7190,5182],{"class":955},[938,7192,1148],{"class":944},[938,7194,6018],{"class":1072},[938,7196,1131],{"class":944},[938,7198,7199,7201,7203,7205],{"class":940,"line":1066},[938,7200,6025],{"class":1097},[938,7202,5341],{"class":944},[938,7204,5887],{"class":955},[938,7206,5067],{"class":944},[938,7208,7209,7211],{"class":940,"line":1094},[938,7210,5427],{"class":1097},[938,7212,5092],{"class":944},[938,7214,7215],{"class":940,"line":1116},[938,7216,1057],{"emptyLinePlaceholder":1056},[938,7218,7219,7221,7223],{"class":940,"line":1134},[938,7220,6874],{"class":955},[938,7222,1125],{"class":944},[938,7224,1030],{"class":944},[938,7226,7227,7229,7232,7234,7236,7238],{"class":940,"line":1154},[938,7228,3052],{"class":944},[938,7230,7231],{"class":1727},"event",[938,7233,2673],{"class":944},[938,7235,1880],{"class":944},[938,7237,7143],{"class":955},[938,7239,2716],{"class":944},[938,7241,7242,7244,7247,7249,7251,7253],{"class":940,"line":1171},[938,7243,3052],{"class":944},[938,7245,7246],{"class":1727},"entity_id",[938,7248,2673],{"class":944},[938,7250,1880],{"class":944},[938,7252,7152],{"class":955},[938,7254,2716],{"class":944},[938,7256,7257,7259,7262,7264,7266,7268,7271,7273],{"class":940,"line":1191},[938,7258,3052],{"class":944},[938,7260,7261],{"class":1727},"action",[938,7263,2673],{"class":944},[938,7265,1880],{"class":944},[938,7267,2635],{"class":944},[938,7269,7270],{"class":1727},"callback",[938,7272,2673],{"class":944},[938,7274,2716],{"class":944},[938,7276,7277,7279,7282,7284,7286],{"class":940,"line":1196},[938,7278,3052],{"class":944},[938,7280,7281],{"class":1727},"attributes",[938,7283,2673],{"class":944},[938,7285,1880],{"class":944},[938,7287,1030],{"class":944},[938,7289,7290,7293,7295,7297,7299,7301],{"class":940,"line":1231},[938,7291,7292],{"class":944},"            \"",[938,7294,7270],{"class":1727},[938,7296,2673],{"class":944},[938,7298,1880],{"class":944},[938,7300,7161],{"class":955},[938,7302,2716],{"class":944},[938,7304,7305,7307,7310,7312,7314],{"class":940,"line":1237},[938,7306,7292],{"class":944},[938,7308,7309],{"class":1727},"use_tls_12",[938,7311,2673],{"class":944},[938,7313,1880],{"class":944},[938,7315,7316],{"class":944}," True,\n",[938,7318,7319],{"class":940,"line":1243},[938,7320,7321],{"class":944},"        },\n",[938,7323,7324],{"class":940,"line":2431},[938,7325,1234],{"class":944},[938,7327,7328],{"class":940,"line":2437},[938,7329,1057],{"emptyLinePlaceholder":1056},[938,7331,7332,7335,7337,7339,7341,7343,7345,7347,7349,7351,7353,7355],{"class":940,"line":2443},[938,7333,7334],{"class":955},"    webhook_secret ",[938,7336,1125],{"class":944},[938,7338,5217],{"class":1072},[938,7340,1076],{"class":944},[938,7342,5222],{"class":1072},[938,7344,1085],{"class":944},[938,7346,2635],{"class":944},[938,7348,4713],{"class":1727},[938,7350,2673],{"class":944},[938,7352,1085],{"class":944},[938,7354,5235],{"class":944},[938,7356,3256],{"class":944},[938,7358,7359,7361,7364],{"class":940,"line":2449},[938,7360,6025],{"class":1097},[938,7362,7363],{"class":955}," webhook_secret",[938,7365,5067],{"class":944},[938,7367,7368,7370,7372,7374,7376,7378,7381,7383,7386,7388,7390,7392],{"class":940,"line":2454},[938,7369,6919],{"class":955},[938,7371,5800],{"class":944},[938,7373,2673],{"class":944},[938,7375,7281],{"class":1727},[938,7377,2673],{"class":944},[938,7379,7380],{"class":944},"][",[938,7382,2673],{"class":944},[938,7384,7385],{"class":1727},"secret_key",[938,7387,2673],{"class":944},[938,7389,6931],{"class":944},[938,7391,5214],{"class":944},[938,7393,7394],{"class":955}," webhook_secret\n",[938,7396,7397],{"class":940,"line":2460},[938,7398,1057],{"emptyLinePlaceholder":1056},[938,7400,7401,7403,7405,7407,7409,7411,7413,7415,7417,7419,7421,7423],{"class":940,"line":2466},[938,7402,6047],{"class":1097},[938,7404,2660],{"class":955},[938,7406,1148],{"class":944},[938,7408,5528],{"class":1072},[938,7410,1076],{"class":944},[938,7412,5533],{"class":2690},[938,7414,1125],{"class":944},[938,7416,5538],{"class":1522},[938,7418,1731],{"class":944},[938,7420,5543],{"class":1097},[938,7422,5546],{"class":955},[938,7424,5067],{"class":944},[938,7426,7427,7429,7431,7433,7435,7437],{"class":940,"line":2472},[938,7428,6075],{"class":955},[938,7430,1125],{"class":944},[938,7432,5546],{"class":955},[938,7434,1148],{"class":944},[938,7436,2665],{"class":1072},[938,7438,3210],{"class":944},[938,7440,7441,7443,7445,7447,7449,7451,7453,7455,7458],{"class":940,"line":3049},[938,7442,6090],{"class":1035},[938,7444,2673],{"class":1727},[938,7446,2676],{"class":1522},[938,7448,5154],{"class":955},[938,7450,1148],{"class":944},[938,7452,5211],{"class":948},[938,7454,2682],{"class":1522},[938,7456,7457],{"class":1727},"/v2/events\"",[938,7459,2716],{"class":944},[938,7461,7462,7464,7466,7468,7470,7472,7474,7476,7478,7480,7482,7484,7486,7488,7490,7492],{"class":940,"line":3071},[938,7463,6112],{"class":2690},[938,7465,6396],{"class":944},[938,7467,5154],{"class":955},[938,7469,1148],{"class":944},[938,7471,6121],{"class":1072},[938,7473,1076],{"class":944},[938,7475,2762],{"class":1072},[938,7477,6409],{"class":944},[938,7479,2635],{"class":944},[938,7481,5727],{"class":1727},[938,7483,2673],{"class":944},[938,7485,1880],{"class":944},[938,7487,2635],{"class":944},[938,7489,6422],{"class":1727},[938,7491,2673],{"class":944},[938,7493,2951],{"class":944},[938,7495,7496,7498,7500,7502],{"class":940,"line":3149},[938,7497,6431],{"class":2690},[938,7499,1125],{"class":944},[938,7501,7081],{"class":1072},[938,7503,2716],{"class":944},[938,7505,7506],{"class":940,"line":3154},[938,7507,6170],{"class":944},[938,7509,7510,7512,7514,7516],{"class":940,"line":3159},[938,7511,6175],{"class":955},[938,7513,1148],{"class":944},[938,7515,5762],{"class":1072},[938,7517,1131],{"class":944},[938,7519,7520,7522],{"class":940,"line":3185},[938,7521,5427],{"class":1097},[938,7523,5197],{"class":944},[747,7525,7527],{"id":7526},"step-3-track-document-state-with-a-django-model","Step 3: Track document state with a Django model",[752,7529,7530,7531,7533,7534,7537],{},"We need to track each document's journey through the signing pipeline. The ",[920,7532,4594],{}," model uses Django's ",[920,7535,7536],{},"GenericForeignKey"," so it can be attached to any model in the system - a deposit confirmation, an NDA, a beta testing agreement, etc.",[929,7539,7542],{"className":2609,"code":7540,"filename":7541,"language":2611,"meta":934,"style":934},"from django.contrib.contenttypes.fields import GenericForeignKey\nfrom django.contrib.contenttypes.models import ContentType\nfrom django.db import models\n\n\nclass SignableDocument(models.Model):\n    \"\"\"Tracks a document uploaded to SignNow for e-signature.\"\"\"\n\n    class Status(models.TextChoices):\n        PENDING = \"pending\", \"Pending Upload\"\n        UPLOADED = \"uploaded\", \"Uploaded to SignNow\"\n        INVITE_SENT = \"invite_sent\", \"Invite Sent\"\n        SIGNED = \"signed\", \"Signed\"\n        DOWNLOADED = \"downloaded\", \"Signed PDF Downloaded\"\n        FAILED = \"failed\", \"Failed\"\n\n    # Generic relation - attach to any Django model\n    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)\n    object_id = models.PositiveIntegerField()\n    source_document = GenericForeignKey(\"content_type\", \"object_id\")\n\n    # SignNow tracking\n    signnow_document_id = models.CharField(max_length=64, blank=True, db_index=True)\n    signnow_invite_id = models.CharField(max_length=64, blank=True)\n\n    # Signer info\n    signer_email = models.EmailField()\n    signer_name = models.CharField(max_length=255, blank=True)\n\n    # Status & errors\n    status = models.CharField(max_length=16, choices=Status.choices, default=Status.PENDING)\n    error_message = models.TextField(blank=True)\n\n    # Files\n    original_pdf_name = models.CharField(max_length=512, blank=True)\n    signed_pdf = models.FileField(upload_to=\"signnow/signed/%Y/%m/\", blank=True)\n\n    # Timestamps\n    uploaded_at = models.DateTimeField(null=True, blank=True)\n    invite_sent_at = models.DateTimeField(null=True, blank=True)\n    signed_at = models.DateTimeField(null=True, blank=True)\n    downloaded_at = models.DateTimeField(null=True, blank=True)\n","apps/signnow/models.py",[920,7543,7544,7570,7594,7610,7614,7618,7638,7647,7651,7670,7693,7716,7739,7762,7785,7808,7812,7817,7852,7868,7898,7902,7907,7945,7972,7976,7981,7997,8025,8029,8034,8086,8107,8111,8116,8144,8178,8182,8187,8212,8235,8258],{"__ignoreMap":934},[938,7545,7546,7548,7550,7552,7555,7557,7560,7562,7565,7567],{"class":940,"line":941},[938,7547,4984],{"class":1097},[938,7549,4987],{"class":955},[938,7551,1148],{"class":944},[938,7553,7554],{"class":955},"contrib",[938,7556,1148],{"class":944},[938,7558,7559],{"class":955},"contenttypes",[938,7561,1148],{"class":944},[938,7563,7564],{"class":955},"fields ",[938,7566,2618],{"class":1097},[938,7568,7569],{"class":955}," GenericForeignKey\n",[938,7571,7572,7574,7576,7578,7580,7582,7584,7586,7589,7591],{"class":940,"line":967},[938,7573,4984],{"class":1097},[938,7575,4987],{"class":955},[938,7577,1148],{"class":944},[938,7579,7554],{"class":955},[938,7581,1148],{"class":944},[938,7583,7559],{"class":955},[938,7585,1148],{"class":944},[938,7587,7588],{"class":955},"models ",[938,7590,2618],{"class":1097},[938,7592,7593],{"class":955}," ContentType\n",[938,7595,7596,7598,7600,7602,7605,7607],{"class":940,"line":1053},[938,7597,4984],{"class":1097},[938,7599,4987],{"class":955},[938,7601,1148],{"class":944},[938,7603,7604],{"class":955},"db ",[938,7606,2618],{"class":1097},[938,7608,7609],{"class":955}," models\n",[938,7611,7612],{"class":940,"line":1060},[938,7613,1057],{"emptyLinePlaceholder":1056},[938,7615,7616],{"class":940,"line":1066},[938,7617,1057],{"emptyLinePlaceholder":1056},[938,7619,7620,7622,7625,7627,7630,7632,7635],{"class":940,"line":1094},[938,7621,1017],{"class":1035},[938,7623,7624],{"class":1020}," SignableDocument",[938,7626,1076],{"class":944},[938,7628,7629],{"class":1020},"models",[938,7631,1148],{"class":944},[938,7633,7634],{"class":1020},"Model",[938,7636,7637],{"class":944},"):\n",[938,7639,7640,7642,7645],{"class":940,"line":1116},[938,7641,5072],{"class":1097},[938,7643,7644],{"class":1535},"Tracks a document uploaded to SignNow for e-signature.",[938,7646,5078],{"class":1097},[938,7648,7649],{"class":940,"line":1134},[938,7650,1057],{"emptyLinePlaceholder":1056},[938,7652,7653,7656,7659,7661,7663,7665,7668],{"class":940,"line":1154},[938,7654,7655],{"class":1035},"    class",[938,7657,7658],{"class":1020}," Status",[938,7660,1076],{"class":944},[938,7662,7629],{"class":1020},[938,7664,1148],{"class":944},[938,7666,7667],{"class":1020},"TextChoices",[938,7669,7637],{"class":944},[938,7671,7672,7675,7677,7679,7682,7684,7686,7688,7691],{"class":940,"line":1171},[938,7673,7674],{"class":955},"        PENDING ",[938,7676,1125],{"class":944},[938,7678,2635],{"class":944},[938,7680,7681],{"class":1727},"pending",[938,7683,2673],{"class":944},[938,7685,1085],{"class":944},[938,7687,2635],{"class":944},[938,7689,7690],{"class":1727},"Pending Upload",[938,7692,2641],{"class":944},[938,7694,7695,7698,7700,7702,7705,7707,7709,7711,7714],{"class":940,"line":1191},[938,7696,7697],{"class":955},"        UPLOADED ",[938,7699,1125],{"class":944},[938,7701,2635],{"class":944},[938,7703,7704],{"class":1727},"uploaded",[938,7706,2673],{"class":944},[938,7708,1085],{"class":944},[938,7710,2635],{"class":944},[938,7712,7713],{"class":1727},"Uploaded to SignNow",[938,7715,2641],{"class":944},[938,7717,7718,7721,7723,7725,7728,7730,7732,7734,7737],{"class":940,"line":1196},[938,7719,7720],{"class":955},"        INVITE_SENT ",[938,7722,1125],{"class":944},[938,7724,2635],{"class":944},[938,7726,7727],{"class":1727},"invite_sent",[938,7729,2673],{"class":944},[938,7731,1085],{"class":944},[938,7733,2635],{"class":944},[938,7735,7736],{"class":1727},"Invite Sent",[938,7738,2641],{"class":944},[938,7740,7741,7744,7746,7748,7751,7753,7755,7757,7760],{"class":940,"line":1231},[938,7742,7743],{"class":955},"        SIGNED ",[938,7745,1125],{"class":944},[938,7747,2635],{"class":944},[938,7749,7750],{"class":1727},"signed",[938,7752,2673],{"class":944},[938,7754,1085],{"class":944},[938,7756,2635],{"class":944},[938,7758,7759],{"class":1727},"Signed",[938,7761,2641],{"class":944},[938,7763,7764,7767,7769,7771,7774,7776,7778,7780,7783],{"class":940,"line":1237},[938,7765,7766],{"class":955},"        DOWNLOADED ",[938,7768,1125],{"class":944},[938,7770,2635],{"class":944},[938,7772,7773],{"class":1727},"downloaded",[938,7775,2673],{"class":944},[938,7777,1085],{"class":944},[938,7779,2635],{"class":944},[938,7781,7782],{"class":1727},"Signed PDF Downloaded",[938,7784,2641],{"class":944},[938,7786,7787,7790,7792,7794,7797,7799,7801,7803,7806],{"class":940,"line":1243},[938,7788,7789],{"class":955},"        FAILED ",[938,7791,1125],{"class":944},[938,7793,2635],{"class":944},[938,7795,7796],{"class":1727},"failed",[938,7798,2673],{"class":944},[938,7800,1085],{"class":944},[938,7802,2635],{"class":944},[938,7804,7805],{"class":1727},"Failed",[938,7807,2641],{"class":944},[938,7809,7810],{"class":940,"line":2431},[938,7811,1057],{"emptyLinePlaceholder":1056},[938,7813,7814],{"class":940,"line":2437},[938,7815,7816],{"class":1535},"    # Generic relation - attach to any Django model\n",[938,7818,7819,7822,7824,7827,7829,7832,7834,7836,7838,7841,7843,7845,7847,7850],{"class":940,"line":2443},[938,7820,7821],{"class":955},"    content_type ",[938,7823,1125],{"class":944},[938,7825,7826],{"class":955}," models",[938,7828,1148],{"class":944},[938,7830,7831],{"class":1072},"ForeignKey",[938,7833,1076],{"class":944},[938,7835,4598],{"class":1072},[938,7837,1085],{"class":944},[938,7839,7840],{"class":2690}," on_delete",[938,7842,1125],{"class":944},[938,7844,7629],{"class":1072},[938,7846,1148],{"class":944},[938,7848,7849],{"class":948},"CASCADE",[938,7851,3256],{"class":944},[938,7853,7854,7857,7859,7861,7863,7866],{"class":940,"line":2449},[938,7855,7856],{"class":955},"    object_id ",[938,7858,1125],{"class":944},[938,7860,7826],{"class":955},[938,7862,1148],{"class":944},[938,7864,7865],{"class":1072},"PositiveIntegerField",[938,7867,1131],{"class":944},[938,7869,7870,7873,7875,7878,7880,7882,7885,7887,7889,7891,7894,7896],{"class":940,"line":2454},[938,7871,7872],{"class":955},"    source_document ",[938,7874,1125],{"class":944},[938,7876,7877],{"class":1072}," GenericForeignKey",[938,7879,1076],{"class":944},[938,7881,2673],{"class":944},[938,7883,7884],{"class":1727},"content_type",[938,7886,2673],{"class":944},[938,7888,1085],{"class":944},[938,7890,2635],{"class":944},[938,7892,7893],{"class":1727},"object_id",[938,7895,2673],{"class":944},[938,7897,3256],{"class":944},[938,7899,7900],{"class":940,"line":2460},[938,7901,1057],{"emptyLinePlaceholder":1056},[938,7903,7904],{"class":940,"line":2466},[938,7905,7906],{"class":1535},"    # SignNow tracking\n",[938,7908,7909,7912,7914,7916,7918,7921,7923,7926,7928,7931,7933,7936,7939,7942],{"class":940,"line":2472},[938,7910,7911],{"class":955},"    signnow_document_id ",[938,7913,1125],{"class":944},[938,7915,7826],{"class":955},[938,7917,1148],{"class":944},[938,7919,7920],{"class":1072},"CharField",[938,7922,1076],{"class":944},[938,7924,7925],{"class":2690},"max_length",[938,7927,1125],{"class":944},[938,7929,7930],{"class":1522},"64",[938,7932,1085],{"class":944},[938,7934,7935],{"class":2690}," blank",[938,7937,7938],{"class":944},"=True,",[938,7940,7941],{"class":2690}," db_index",[938,7943,7944],{"class":944},"=True)\n",[938,7946,7947,7950,7952,7954,7956,7958,7960,7962,7964,7966,7968,7970],{"class":940,"line":3049},[938,7948,7949],{"class":955},"    signnow_invite_id ",[938,7951,1125],{"class":944},[938,7953,7826],{"class":955},[938,7955,1148],{"class":944},[938,7957,7920],{"class":1072},[938,7959,1076],{"class":944},[938,7961,7925],{"class":2690},[938,7963,1125],{"class":944},[938,7965,7930],{"class":1522},[938,7967,1085],{"class":944},[938,7969,7935],{"class":2690},[938,7971,7944],{"class":944},[938,7973,7974],{"class":940,"line":3071},[938,7975,1057],{"emptyLinePlaceholder":1056},[938,7977,7978],{"class":940,"line":3149},[938,7979,7980],{"class":1535},"    # Signer info\n",[938,7982,7983,7986,7988,7990,7992,7995],{"class":940,"line":3154},[938,7984,7985],{"class":955},"    signer_email ",[938,7987,1125],{"class":944},[938,7989,7826],{"class":955},[938,7991,1148],{"class":944},[938,7993,7994],{"class":1072},"EmailField",[938,7996,1131],{"class":944},[938,7998,7999,8002,8004,8006,8008,8010,8012,8014,8016,8019,8021,8023],{"class":940,"line":3159},[938,8000,8001],{"class":955},"    signer_name ",[938,8003,1125],{"class":944},[938,8005,7826],{"class":955},[938,8007,1148],{"class":944},[938,8009,7920],{"class":1072},[938,8011,1076],{"class":944},[938,8013,7925],{"class":2690},[938,8015,1125],{"class":944},[938,8017,8018],{"class":1522},"255",[938,8020,1085],{"class":944},[938,8022,7935],{"class":2690},[938,8024,7944],{"class":944},[938,8026,8027],{"class":940,"line":3185},[938,8028,1057],{"emptyLinePlaceholder":1056},[938,8030,8031],{"class":940,"line":3190},[938,8032,8033],{"class":1535},"    # Status & errors\n",[938,8035,8036,8039,8041,8043,8045,8047,8049,8051,8053,8056,8058,8061,8063,8066,8068,8071,8073,8075,8077,8079,8081,8084],{"class":940,"line":3196},[938,8037,8038],{"class":955},"    status ",[938,8040,1125],{"class":944},[938,8042,7826],{"class":955},[938,8044,1148],{"class":944},[938,8046,7920],{"class":1072},[938,8048,1076],{"class":944},[938,8050,7925],{"class":2690},[938,8052,1125],{"class":944},[938,8054,8055],{"class":1522},"16",[938,8057,1085],{"class":944},[938,8059,8060],{"class":2690}," choices",[938,8062,1125],{"class":944},[938,8064,8065],{"class":1072},"Status",[938,8067,1148],{"class":944},[938,8069,8070],{"class":948},"choices",[938,8072,1085],{"class":944},[938,8074,4784],{"class":2690},[938,8076,1125],{"class":944},[938,8078,8065],{"class":1072},[938,8080,1148],{"class":944},[938,8082,8083],{"class":948},"PENDING",[938,8085,3256],{"class":944},[938,8087,8088,8091,8093,8095,8097,8100,8102,8105],{"class":940,"line":3213},[938,8089,8090],{"class":955},"    error_message ",[938,8092,1125],{"class":944},[938,8094,7826],{"class":955},[938,8096,1148],{"class":944},[938,8098,8099],{"class":1072},"TextField",[938,8101,1076],{"class":944},[938,8103,8104],{"class":2690},"blank",[938,8106,7944],{"class":944},[938,8108,8109],{"class":940,"line":3242},[938,8110,1057],{"emptyLinePlaceholder":1056},[938,8112,8113],{"class":940,"line":3253},[938,8114,8115],{"class":1535},"    # Files\n",[938,8117,8118,8121,8123,8125,8127,8129,8131,8133,8135,8138,8140,8142],{"class":940,"line":3259},[938,8119,8120],{"class":955},"    original_pdf_name ",[938,8122,1125],{"class":944},[938,8124,7826],{"class":955},[938,8126,1148],{"class":944},[938,8128,7920],{"class":1072},[938,8130,1076],{"class":944},[938,8132,7925],{"class":2690},[938,8134,1125],{"class":944},[938,8136,8137],{"class":1522},"512",[938,8139,1085],{"class":944},[938,8141,7935],{"class":2690},[938,8143,7944],{"class":944},[938,8145,8146,8149,8151,8153,8155,8158,8160,8163,8165,8167,8170,8172,8174,8176],{"class":940,"line":3264},[938,8147,8148],{"class":955},"    signed_pdf ",[938,8150,1125],{"class":944},[938,8152,7826],{"class":955},[938,8154,1148],{"class":944},[938,8156,8157],{"class":1072},"FileField",[938,8159,1076],{"class":944},[938,8161,8162],{"class":2690},"upload_to",[938,8164,1125],{"class":944},[938,8166,2673],{"class":944},[938,8168,8169],{"class":1727},"signnow/signed/%Y/%m/",[938,8171,2673],{"class":944},[938,8173,1085],{"class":944},[938,8175,7935],{"class":2690},[938,8177,7944],{"class":944},[938,8179,8180],{"class":940,"line":3270},[938,8181,1057],{"emptyLinePlaceholder":1056},[938,8183,8184],{"class":940,"line":3294},[938,8185,8186],{"class":1535},"    # Timestamps\n",[938,8188,8189,8192,8194,8196,8198,8201,8203,8206,8208,8210],{"class":940,"line":3321},[938,8190,8191],{"class":955},"    uploaded_at ",[938,8193,1125],{"class":944},[938,8195,7826],{"class":955},[938,8197,1148],{"class":944},[938,8199,8200],{"class":1072},"DateTimeField",[938,8202,1076],{"class":944},[938,8204,8205],{"class":2690},"null",[938,8207,7938],{"class":944},[938,8209,7935],{"class":2690},[938,8211,7944],{"class":944},[938,8213,8214,8217,8219,8221,8223,8225,8227,8229,8231,8233],{"class":940,"line":3333},[938,8215,8216],{"class":955},"    invite_sent_at ",[938,8218,1125],{"class":944},[938,8220,7826],{"class":955},[938,8222,1148],{"class":944},[938,8224,8200],{"class":1072},[938,8226,1076],{"class":944},[938,8228,8205],{"class":2690},[938,8230,7938],{"class":944},[938,8232,7935],{"class":2690},[938,8234,7944],{"class":944},[938,8236,8237,8240,8242,8244,8246,8248,8250,8252,8254,8256],{"class":940,"line":3356},[938,8238,8239],{"class":955},"    signed_at ",[938,8241,1125],{"class":944},[938,8243,7826],{"class":955},[938,8245,1148],{"class":944},[938,8247,8200],{"class":1072},[938,8249,1076],{"class":944},[938,8251,8205],{"class":2690},[938,8253,7938],{"class":944},[938,8255,7935],{"class":2690},[938,8257,7944],{"class":944},[938,8259,8260,8263,8265,8267,8269,8271,8273,8275,8277,8279],{"class":940,"line":5513},[938,8261,8262],{"class":955},"    downloaded_at ",[938,8264,1125],{"class":944},[938,8266,7826],{"class":955},[938,8268,1148],{"class":944},[938,8270,8200],{"class":1072},[938,8272,1076],{"class":944},[938,8274,8205],{"class":2690},[938,8276,7938],{"class":944},[938,8278,7935],{"class":2690},[938,8280,7944],{"class":944},[752,8282,8283],{},"The status flow is linear and predictable:",[929,8285,8290],{"className":8286,"code":8288,"language":8289},[8287],"language-text","PENDING → UPLOADED → INVITE_SENT → SIGNED → DOWNLOADED\n                                                ↘ FAILED (at any step)\n","text",[920,8291,8288],{"__ignoreMap":934},[747,8293,8295],{"id":8294},"step-4-build-the-async-signing-pipeline-with-celery","Step 4: Build the async signing pipeline with Celery",[752,8297,8298],{},"This is where everything comes together. A single Celery task orchestrates the entire signing flow - upload the document, add signature fields, send the invite, and register the webhook:",[929,8300,8303],{"className":2609,"code":8301,"filename":8302,"language":2611,"meta":934,"style":934},"from celery import shared_task\n\n# Signature field position (calibrate for your PDF layout)\nTESTER_SIGNATURE_FIELD = {\n    \"x\": 350,\n    \"y\": 700,\n    \"width\": 200,\n    \"height\": 50,\n    \"page_number\": 0,\n    \"type\": \"signature\",\n    \"role\": \"Signer 1\",\n    \"required\": True,\n    \"label\": \"Tester Signature\",\n}\n\n\n@shared_task(bind=True, max_retries=3, default_retry_delay=60)\ndef upload_and_send_for_signature(self, signable_document_id: int):\n    \"\"\"Full pipeline: upload PDF → add fields → send invite → register webhook.\"\"\"\n    from signnow.models import SignableDocument\n    from signnow.services.signnow_service import SignNowService\n\n    signable = SignableDocument.objects.select_related(\"content_type\").get(\n        id=signable_document_id\n    )\n\n    try:\n        # 1. Read PDF from storage\n        source = signable.source_document\n        pdf_bytes = source.pdf_file.read()\n        filename = source.pdf_file.name.split(\"/\")[-1]\n\n        # 2. Upload to SignNow\n        if not signable.signnow_document_id:\n            result = SignNowService.upload_document(pdf_bytes, filename)\n            if not result:\n                raise RuntimeError(\"Failed to upload document to SignNow\")\n\n            signable.signnow_document_id = result[\"id\"]\n            signable.status = SignableDocument.Status.UPLOADED\n            signable.uploaded_at = timezone.now()\n            signable.save(update_fields=[\"signnow_document_id\", \"status\", \"uploaded_at\"])\n\n        # 3. Add signature fields\n        SignNowService.add_signature_fields(\n            signable.signnow_document_id,\n            [TESTER_SIGNATURE_FIELD],\n        )\n\n        # 4. Get role_id (assigned when fields were added)\n        doc_data = SignNowService.get_document(signable.signnow_document_id)\n        roles = doc_data.get(\"roles\", [])\n        signer_role = next(r for r in roles if r.get(\"name\") == \"Signer 1\")\n\n        # 5. Send role-based invite\n        signers = [{\n            \"email\": signable.signer_email,\n            \"role\": \"Signer 1\",\n            \"role_id\": signer_role[\"unique_id\"],\n            \"order\": 1,\n        }]\n\n        invite_result = SignNowService.send_role_based_invite(\n            document_id=signable.signnow_document_id,\n            signers=signers,\n            from_email=settings.SIGNNOW_USERNAME,\n        )\n\n        signable.status = SignableDocument.Status.INVITE_SENT\n        signable.invite_sent_at = timezone.now()\n        signable.save(update_fields=[\"status\", \"invite_sent_at\"])\n\n        # 6. Register webhook for document completion\n        callback_url = settings.SIGNNOW_WEBHOOK_CALLBACK_URL\n        if callback_url:\n            SignNowService.register_webhook(\n                event=\"document.complete\",\n                entity_id=signable.signnow_document_id,\n                callback_url=callback_url,\n            )\n\n    except Exception as exc:\n        if self.request.retries >= self.max_retries:\n            signable.status = SignableDocument.Status.FAILED\n            signable.error_message = str(exc)\n            signable.save(update_fields=[\"status\", \"error_message\"])\n            return\n        self.retry(exc=exc)\n","apps/signnow/tasks.py",[920,8304,8305,8317,8321,8326,8335,8351,8367,8383,8399,8415,8434,8453,8466,8486,8490,8494,8498,8532,8557,8566,8583,8604,8608,8642,8652,8656,8660,8667,8672,8687,8709,8747,8751,8756,8771,8796,8808,8827,8831,8854,8876,8897,8939,8943,8948,8960,8970,8980,8984,8988,8993,9018,9046,9102,9106,9111,9121,9140,9158,9183,9199,9204,9208,9224,9239,9251,9267,9272,9277,9300,9320,9351,9356,9362,9378,9387,9400,9417,9433,9446,9451,9456,9472,9502,9524,9545,9576,9582],{"__ignoreMap":934},[938,8306,8307,8309,8312,8314],{"class":940,"line":941},[938,8308,4984],{"class":1097},[938,8310,8311],{"class":955}," celery ",[938,8313,2618],{"class":1097},[938,8315,8316],{"class":955}," shared_task\n",[938,8318,8319],{"class":940,"line":967},[938,8320,1057],{"emptyLinePlaceholder":1056},[938,8322,8323],{"class":940,"line":1053},[938,8324,8325],{"class":1535},"# Signature field position (calibrate for your PDF layout)\n",[938,8327,8328,8331,8333],{"class":940,"line":1060},[938,8329,8330],{"class":955},"TESTER_SIGNATURE_FIELD ",[938,8332,1125],{"class":944},[938,8334,1030],{"class":944},[938,8336,8337,8339,8342,8344,8346,8349],{"class":940,"line":1066},[938,8338,2699],{"class":944},[938,8340,8341],{"class":1727},"x",[938,8343,2673],{"class":944},[938,8345,1880],{"class":944},[938,8347,8348],{"class":1522}," 350",[938,8350,2716],{"class":944},[938,8352,8353,8355,8358,8360,8362,8365],{"class":940,"line":1094},[938,8354,2699],{"class":944},[938,8356,8357],{"class":1727},"y",[938,8359,2673],{"class":944},[938,8361,1880],{"class":944},[938,8363,8364],{"class":1522}," 700",[938,8366,2716],{"class":944},[938,8368,8369,8371,8374,8376,8378,8381],{"class":940,"line":1116},[938,8370,2699],{"class":944},[938,8372,8373],{"class":1727},"width",[938,8375,2673],{"class":944},[938,8377,1880],{"class":944},[938,8379,8380],{"class":1522}," 200",[938,8382,2716],{"class":944},[938,8384,8385,8387,8390,8392,8394,8397],{"class":940,"line":1134},[938,8386,2699],{"class":944},[938,8388,8389],{"class":1727},"height",[938,8391,2673],{"class":944},[938,8393,1880],{"class":944},[938,8395,8396],{"class":1522}," 50",[938,8398,2716],{"class":944},[938,8400,8401,8403,8406,8408,8410,8413],{"class":940,"line":1154},[938,8402,2699],{"class":944},[938,8404,8405],{"class":1727},"page_number",[938,8407,2673],{"class":944},[938,8409,1880],{"class":944},[938,8411,8412],{"class":1522}," 0",[938,8414,2716],{"class":944},[938,8416,8417,8419,8421,8423,8425,8427,8430,8432],{"class":940,"line":1171},[938,8418,2699],{"class":944},[938,8420,6647],{"class":1727},[938,8422,2673],{"class":944},[938,8424,1880],{"class":944},[938,8426,2635],{"class":944},[938,8428,8429],{"class":1727},"signature",[938,8431,2673],{"class":944},[938,8433,2716],{"class":944},[938,8435,8436,8438,8440,8442,8444,8446,8449,8451],{"class":940,"line":1191},[938,8437,2699],{"class":944},[938,8439,2937],{"class":1727},[938,8441,2673],{"class":944},[938,8443,1880],{"class":944},[938,8445,2635],{"class":944},[938,8447,8448],{"class":1727},"Signer 1",[938,8450,2673],{"class":944},[938,8452,2716],{"class":944},[938,8454,8455,8457,8460,8462,8464],{"class":940,"line":1196},[938,8456,2699],{"class":944},[938,8458,8459],{"class":1727},"required",[938,8461,2673],{"class":944},[938,8463,1880],{"class":944},[938,8465,7316],{"class":944},[938,8467,8468,8470,8473,8475,8477,8479,8482,8484],{"class":940,"line":1231},[938,8469,2699],{"class":944},[938,8471,8472],{"class":1727},"label",[938,8474,2673],{"class":944},[938,8476,1880],{"class":944},[938,8478,2635],{"class":944},[938,8480,8481],{"class":1727},"Tester Signature",[938,8483,2673],{"class":944},[938,8485,2716],{"class":944},[938,8487,8488],{"class":940,"line":1237},[938,8489,1246],{"class":944},[938,8491,8492],{"class":940,"line":1243},[938,8493,1057],{"emptyLinePlaceholder":1056},[938,8495,8496],{"class":940,"line":2431},[938,8497,1057],{"emptyLinePlaceholder":1056},[938,8499,8500,8502,8505,8507,8510,8512,8515,8517,8520,8522,8525,8527,8530],{"class":940,"line":2437},[938,8501,5951],{"class":944},[938,8503,8504],{"class":1072},"shared_task",[938,8506,1076],{"class":944},[938,8508,8509],{"class":2690},"bind",[938,8511,7938],{"class":944},[938,8513,8514],{"class":2690}," max_retries",[938,8516,1125],{"class":944},[938,8518,8519],{"class":1522},"3",[938,8521,1085],{"class":944},[938,8523,8524],{"class":2690}," default_retry_delay",[938,8526,1125],{"class":944},[938,8528,8529],{"class":1522},"60",[938,8531,3256],{"class":944},[938,8533,8534,8536,8539,8541,8545,8547,8550,8552,8555],{"class":940,"line":2443},[938,8535,5958],{"class":1035},[938,8537,8538],{"class":1072}," upload_and_send_for_signature",[938,8540,1076],{"class":944},[938,8542,8544],{"class":8543},"s5tWE","self",[938,8546,1085],{"class":944},[938,8548,8549],{"class":2690}," signable_document_id",[938,8551,1880],{"class":944},[938,8553,8554],{"class":1020}," int",[938,8556,7637],{"class":944},[938,8558,8559,8561,8564],{"class":940,"line":2449},[938,8560,5072],{"class":1097},[938,8562,8563],{"class":1535},"Full pipeline: upload PDF → add fields → send invite → register webhook.",[938,8565,5078],{"class":1097},[938,8567,8568,8571,8574,8576,8578,8580],{"class":940,"line":2454},[938,8569,8570],{"class":1097},"    from",[938,8572,8573],{"class":955}," signnow",[938,8575,1148],{"class":944},[938,8577,7588],{"class":955},[938,8579,2618],{"class":1097},[938,8581,8582],{"class":955}," SignableDocument\n",[938,8584,8585,8587,8589,8591,8594,8596,8599,8601],{"class":940,"line":2460},[938,8586,8570],{"class":1097},[938,8588,8573],{"class":955},[938,8590,1148],{"class":944},[938,8592,8593],{"class":955},"services",[938,8595,1148],{"class":944},[938,8597,8598],{"class":955},"signnow_service ",[938,8600,2618],{"class":1097},[938,8602,8603],{"class":955}," SignNowService\n",[938,8605,8606],{"class":940,"line":2466},[938,8607,1057],{"emptyLinePlaceholder":1056},[938,8609,8610,8613,8615,8617,8619,8622,8624,8627,8629,8631,8633,8635,8638,8640],{"class":940,"line":2472},[938,8611,8612],{"class":955},"    signable ",[938,8614,1125],{"class":944},[938,8616,7624],{"class":955},[938,8618,1148],{"class":944},[938,8620,8621],{"class":948},"objects",[938,8623,1148],{"class":944},[938,8625,8626],{"class":1072},"select_related",[938,8628,1076],{"class":944},[938,8630,2673],{"class":944},[938,8632,7884],{"class":1727},[938,8634,2673],{"class":944},[938,8636,8637],{"class":944},").",[938,8639,5487],{"class":1072},[938,8641,3210],{"class":944},[938,8643,8644,8647,8649],{"class":940,"line":3049},[938,8645,8646],{"class":2690},"        id",[938,8648,1125],{"class":944},[938,8650,8651],{"class":1072},"signable_document_id\n",[938,8653,8654],{"class":940,"line":3071},[938,8655,3359],{"class":944},[938,8657,8658],{"class":940,"line":3149},[938,8659,1057],{"emptyLinePlaceholder":1056},[938,8661,8662,8665],{"class":940,"line":3154},[938,8663,8664],{"class":1097},"    try",[938,8666,5067],{"class":944},[938,8668,8669],{"class":940,"line":3159},[938,8670,8671],{"class":1535},"        # 1. Read PDF from storage\n",[938,8673,8674,8677,8679,8682,8684],{"class":940,"line":3185},[938,8675,8676],{"class":955},"        source ",[938,8678,1125],{"class":944},[938,8680,8681],{"class":955}," signable",[938,8683,1148],{"class":944},[938,8685,8686],{"class":948},"source_document\n",[938,8688,8689,8692,8694,8697,8699,8702,8704,8707],{"class":940,"line":3190},[938,8690,8691],{"class":955},"        pdf_bytes ",[938,8693,1125],{"class":944},[938,8695,8696],{"class":955}," source",[938,8698,1148],{"class":944},[938,8700,8701],{"class":948},"pdf_file",[938,8703,1148],{"class":944},[938,8705,8706],{"class":1072},"read",[938,8708,1131],{"class":944},[938,8710,8711,8714,8716,8718,8720,8722,8724,8726,8728,8731,8733,8735,8737,8739,8742,8745],{"class":940,"line":3196},[938,8712,8713],{"class":955},"        filename ",[938,8715,1125],{"class":944},[938,8717,8696],{"class":955},[938,8719,1148],{"class":944},[938,8721,8701],{"class":948},[938,8723,1148],{"class":944},[938,8725,2919],{"class":948},[938,8727,1148],{"class":944},[938,8729,8730],{"class":1072},"split",[938,8732,1076],{"class":944},[938,8734,2673],{"class":944},[938,8736,1494],{"class":1727},[938,8738,2673],{"class":944},[938,8740,8741],{"class":944},")[-",[938,8743,8744],{"class":1522},"1",[938,8746,2767],{"class":944},[938,8748,8749],{"class":940,"line":3213},[938,8750,1057],{"emptyLinePlaceholder":1056},[938,8752,8753],{"class":940,"line":3242},[938,8754,8755],{"class":1535},"        # 2. Upload to SignNow\n",[938,8757,8758,8760,8762,8764,8766,8769],{"class":940,"line":3253},[938,8759,5179],{"class":1097},[938,8761,5341],{"class":944},[938,8763,8681],{"class":955},[938,8765,1148],{"class":944},[938,8767,8768],{"class":948},"signnow_document_id",[938,8770,5067],{"class":944},[938,8772,8773,8776,8778,8780,8782,8785,8787,8790,8792,8794],{"class":940,"line":3259},[938,8774,8775],{"class":955},"            result ",[938,8777,1125],{"class":944},[938,8779,5064],{"class":955},[938,8781,1148],{"class":944},[938,8783,8784],{"class":1072},"upload_document",[938,8786,1076],{"class":944},[938,8788,8789],{"class":1072},"pdf_bytes",[938,8791,1085],{"class":944},[938,8793,5980],{"class":1072},[938,8795,3256],{"class":944},[938,8797,8798,8801,8803,8806],{"class":940,"line":3264},[938,8799,8800],{"class":1097},"            if",[938,8802,5341],{"class":944},[938,8804,8805],{"class":955}," result",[938,8807,5067],{"class":944},[938,8809,8810,8813,8816,8818,8820,8823,8825],{"class":940,"line":3270},[938,8811,8812],{"class":1097},"                raise",[938,8814,8815],{"class":1020}," RuntimeError",[938,8817,1076],{"class":944},[938,8819,2673],{"class":944},[938,8821,8822],{"class":1727},"Failed to upload document to SignNow",[938,8824,2673],{"class":944},[938,8826,3256],{"class":944},[938,8828,8829],{"class":940,"line":3294},[938,8830,1057],{"emptyLinePlaceholder":1056},[938,8832,8833,8836,8838,8840,8842,8844,8846,8848,8850,8852],{"class":940,"line":3321},[938,8834,8835],{"class":955},"            signable",[938,8837,1148],{"class":944},[938,8839,8768],{"class":948},[938,8841,5214],{"class":944},[938,8843,8805],{"class":955},[938,8845,5800],{"class":944},[938,8847,2673],{"class":944},[938,8849,3178],{"class":1727},[938,8851,2673],{"class":944},[938,8853,2767],{"class":944},[938,8855,8856,8858,8860,8863,8865,8867,8869,8871,8873],{"class":940,"line":3333},[938,8857,8835],{"class":955},[938,8859,1148],{"class":944},[938,8861,8862],{"class":948},"status",[938,8864,5214],{"class":944},[938,8866,7624],{"class":955},[938,8868,1148],{"class":944},[938,8870,8065],{"class":948},[938,8872,1148],{"class":944},[938,8874,8875],{"class":948},"UPLOADED\n",[938,8877,8878,8880,8882,8885,8887,8890,8892,8895],{"class":940,"line":3356},[938,8879,8835],{"class":955},[938,8881,1148],{"class":944},[938,8883,8884],{"class":948},"uploaded_at",[938,8886,5214],{"class":944},[938,8888,8889],{"class":955}," timezone",[938,8891,1148],{"class":944},[938,8893,8894],{"class":1072},"now",[938,8896,1131],{"class":944},[938,8898,8899,8901,8903,8906,8908,8911,8914,8916,8918,8920,8922,8924,8926,8928,8930,8932,8934,8936],{"class":940,"line":5513},[938,8900,8835],{"class":955},[938,8902,1148],{"class":944},[938,8904,8905],{"class":1072},"save",[938,8907,1076],{"class":944},[938,8909,8910],{"class":2690},"update_fields",[938,8912,8913],{"class":944},"=[",[938,8915,2673],{"class":944},[938,8917,8768],{"class":1727},[938,8919,2673],{"class":944},[938,8921,1085],{"class":944},[938,8923,2635],{"class":944},[938,8925,8862],{"class":1727},[938,8927,2673],{"class":944},[938,8929,1085],{"class":944},[938,8931,2635],{"class":944},[938,8933,8884],{"class":1727},[938,8935,2673],{"class":944},[938,8937,8938],{"class":944},"])\n",[938,8940,8941],{"class":940,"line":5518},[938,8942,1057],{"emptyLinePlaceholder":1056},[938,8944,8945],{"class":940,"line":5551},[938,8946,8947],{"class":1535},"        # 3. Add signature fields\n",[938,8949,8950,8953,8955,8958],{"class":940,"line":5567},[938,8951,8952],{"class":955},"        SignNowService",[938,8954,1148],{"class":944},[938,8956,8957],{"class":1072},"add_signature_fields",[938,8959,3210],{"class":944},[938,8961,8962,8964,8966,8968],{"class":940,"line":5590},[938,8963,8835],{"class":1072},[938,8965,1148],{"class":944},[938,8967,8768],{"class":948},[938,8969,2716],{"class":944},[938,8971,8972,8975,8978],{"class":940,"line":5598},[938,8973,8974],{"class":944},"            [",[938,8976,8977],{"class":1072},"TESTER_SIGNATURE_FIELD",[938,8979,6763],{"class":944},[938,8981,8982],{"class":940,"line":5619},[938,8983,6170],{"class":944},[938,8985,8986],{"class":940,"line":5639},[938,8987,1057],{"emptyLinePlaceholder":1056},[938,8989,8990],{"class":940,"line":5658},[938,8991,8992],{"class":1535},"        # 4. Get role_id (assigned when fields were added)\n",[938,8994,8995,8998,9000,9002,9004,9007,9009,9012,9014,9016],{"class":940,"line":5678},[938,8996,8997],{"class":955},"        doc_data ",[938,8999,1125],{"class":944},[938,9001,5064],{"class":955},[938,9003,1148],{"class":944},[938,9005,9006],{"class":1072},"get_document",[938,9008,1076],{"class":944},[938,9010,9011],{"class":1072},"signable",[938,9013,1148],{"class":944},[938,9015,8768],{"class":948},[938,9017,3256],{"class":944},[938,9019,9020,9023,9025,9028,9030,9032,9034,9036,9039,9041,9043],{"class":940,"line":5684},[938,9021,9022],{"class":955},"        roles ",[938,9024,1125],{"class":944},[938,9026,9027],{"class":955}," doc_data",[938,9029,1148],{"class":944},[938,9031,5487],{"class":1072},[938,9033,1076],{"class":944},[938,9035,2673],{"class":944},[938,9037,9038],{"class":1727},"roles",[938,9040,2673],{"class":944},[938,9042,1085],{"class":944},[938,9044,9045],{"class":944}," [])\n",[938,9047,9048,9051,9053,9056,9058,9061,9064,9067,9069,9072,9074,9077,9079,9081,9083,9085,9087,9089,9091,9094,9096,9098,9100],{"class":940,"line":5692},[938,9049,9050],{"class":955},"        signer_role ",[938,9052,1125],{"class":944},[938,9054,9055],{"class":1072}," next",[938,9057,1076],{"class":944},[938,9059,9060],{"class":1072},"r ",[938,9062,9063],{"class":1097},"for",[938,9065,9066],{"class":1072}," r ",[938,9068,1110],{"class":1097},[938,9070,9071],{"class":1072}," roles ",[938,9073,1676],{"class":1097},[938,9075,9076],{"class":1072}," r",[938,9078,1148],{"class":944},[938,9080,5487],{"class":1072},[938,9082,1076],{"class":944},[938,9084,2673],{"class":944},[938,9086,2919],{"class":1727},[938,9088,2673],{"class":944},[938,9090,1731],{"class":944},[938,9092,9093],{"class":944}," ==",[938,9095,2635],{"class":944},[938,9097,8448],{"class":1727},[938,9099,2673],{"class":944},[938,9101,3256],{"class":944},[938,9103,9104],{"class":940,"line":5722},[938,9105,1057],{"emptyLinePlaceholder":1056},[938,9107,9108],{"class":940,"line":5743},[938,9109,9110],{"class":1535},"        # 5. Send role-based invite\n",[938,9112,9113,9116,9118],{"class":940,"line":5748},[938,9114,9115],{"class":955},"        signers ",[938,9117,1125],{"class":944},[938,9119,9120],{"class":944}," [{\n",[938,9122,9123,9125,9127,9129,9131,9133,9135,9138],{"class":940,"line":5754},[938,9124,7292],{"class":944},[938,9126,2702],{"class":1727},[938,9128,2673],{"class":944},[938,9130,1880],{"class":944},[938,9132,8681],{"class":955},[938,9134,1148],{"class":944},[938,9136,9137],{"class":948},"signer_email",[938,9139,2716],{"class":944},[938,9141,9142,9144,9146,9148,9150,9152,9154,9156],{"class":940,"line":5767},[938,9143,7292],{"class":944},[938,9145,2937],{"class":1727},[938,9147,2673],{"class":944},[938,9149,1880],{"class":944},[938,9151,2635],{"class":944},[938,9153,8448],{"class":1727},[938,9155,2673],{"class":944},[938,9157,2716],{"class":944},[938,9159,9160,9162,9165,9167,9169,9172,9174,9176,9179,9181],{"class":940,"line":5784},[938,9161,7292],{"class":944},[938,9163,9164],{"class":1727},"role_id",[938,9166,2673],{"class":944},[938,9168,1880],{"class":944},[938,9170,9171],{"class":955}," signer_role",[938,9173,5800],{"class":944},[938,9175,2673],{"class":944},[938,9177,9178],{"class":1727},"unique_id",[938,9180,2673],{"class":944},[938,9182,6763],{"class":944},[938,9184,9185,9187,9190,9192,9194,9197],{"class":940,"line":5789},[938,9186,7292],{"class":944},[938,9188,9189],{"class":1727},"order",[938,9191,2673],{"class":944},[938,9193,1880],{"class":944},[938,9195,9196],{"class":1522}," 1",[938,9198,2716],{"class":944},[938,9200,9201],{"class":940,"line":5812},[938,9202,9203],{"class":944},"        }]\n",[938,9205,9206],{"class":940,"line":5842},[938,9207,1057],{"emptyLinePlaceholder":1056},[938,9209,9210,9213,9215,9217,9219,9222],{"class":940,"line":5870},[938,9211,9212],{"class":955},"        invite_result ",[938,9214,1125],{"class":944},[938,9216,5064],{"class":955},[938,9218,1148],{"class":944},[938,9220,9221],{"class":1072},"send_role_based_invite",[938,9223,3210],{"class":944},[938,9225,9226,9229,9231,9233,9235,9237],{"class":940,"line":5902},[938,9227,9228],{"class":2690},"            document_id",[938,9230,1125],{"class":944},[938,9232,9011],{"class":1072},[938,9234,1148],{"class":944},[938,9236,8768],{"class":948},[938,9238,2716],{"class":944},[938,9240,9241,9244,9246,9249],{"class":940,"line":5907},[938,9242,9243],{"class":2690},"            signers",[938,9245,1125],{"class":944},[938,9247,9248],{"class":1072},"signers",[938,9250,2716],{"class":944},[938,9252,9254,9257,9259,9261,9263,9265],{"class":940,"line":9253},66,[938,9255,9256],{"class":2690},"            from_email",[938,9258,1125],{"class":944},[938,9260,5222],{"class":1072},[938,9262,1148],{"class":944},[938,9264,4693],{"class":948},[938,9266,2716],{"class":944},[938,9268,9270],{"class":940,"line":9269},67,[938,9271,6170],{"class":944},[938,9273,9275],{"class":940,"line":9274},68,[938,9276,1057],{"emptyLinePlaceholder":1056},[938,9278,9280,9283,9285,9287,9289,9291,9293,9295,9297],{"class":940,"line":9279},69,[938,9281,9282],{"class":955},"        signable",[938,9284,1148],{"class":944},[938,9286,8862],{"class":948},[938,9288,5214],{"class":944},[938,9290,7624],{"class":955},[938,9292,1148],{"class":944},[938,9294,8065],{"class":948},[938,9296,1148],{"class":944},[938,9298,9299],{"class":948},"INVITE_SENT\n",[938,9301,9303,9305,9307,9310,9312,9314,9316,9318],{"class":940,"line":9302},70,[938,9304,9282],{"class":955},[938,9306,1148],{"class":944},[938,9308,9309],{"class":948},"invite_sent_at",[938,9311,5214],{"class":944},[938,9313,8889],{"class":955},[938,9315,1148],{"class":944},[938,9317,8894],{"class":1072},[938,9319,1131],{"class":944},[938,9321,9323,9325,9327,9329,9331,9333,9335,9337,9339,9341,9343,9345,9347,9349],{"class":940,"line":9322},71,[938,9324,9282],{"class":955},[938,9326,1148],{"class":944},[938,9328,8905],{"class":1072},[938,9330,1076],{"class":944},[938,9332,8910],{"class":2690},[938,9334,8913],{"class":944},[938,9336,2673],{"class":944},[938,9338,8862],{"class":1727},[938,9340,2673],{"class":944},[938,9342,1085],{"class":944},[938,9344,2635],{"class":944},[938,9346,9309],{"class":1727},[938,9348,2673],{"class":944},[938,9350,8938],{"class":944},[938,9352,9354],{"class":940,"line":9353},72,[938,9355,1057],{"emptyLinePlaceholder":1056},[938,9357,9359],{"class":940,"line":9358},73,[938,9360,9361],{"class":1535},"        # 6. Register webhook for document completion\n",[938,9363,9365,9368,9370,9373,9375],{"class":940,"line":9364},74,[938,9366,9367],{"class":955},"        callback_url ",[938,9369,1125],{"class":944},[938,9371,9372],{"class":955}," settings",[938,9374,1148],{"class":944},[938,9376,9377],{"class":948},"SIGNNOW_WEBHOOK_CALLBACK_URL\n",[938,9379,9381,9383,9385],{"class":940,"line":9380},75,[938,9382,5179],{"class":1097},[938,9384,7161],{"class":955},[938,9386,5067],{"class":944},[938,9388,9390,9393,9395,9398],{"class":940,"line":9389},76,[938,9391,9392],{"class":955},"            SignNowService",[938,9394,1148],{"class":944},[938,9396,9397],{"class":1072},"register_webhook",[938,9399,3210],{"class":944},[938,9401,9403,9406,9408,9410,9413,9415],{"class":940,"line":9402},77,[938,9404,9405],{"class":2690},"                event",[938,9407,1125],{"class":944},[938,9409,2673],{"class":944},[938,9411,9412],{"class":1727},"document.complete",[938,9414,2673],{"class":944},[938,9416,2716],{"class":944},[938,9418,9420,9423,9425,9427,9429,9431],{"class":940,"line":9419},78,[938,9421,9422],{"class":2690},"                entity_id",[938,9424,1125],{"class":944},[938,9426,9011],{"class":1072},[938,9428,1148],{"class":944},[938,9430,8768],{"class":948},[938,9432,2716],{"class":944},[938,9434,9436,9439,9441,9444],{"class":940,"line":9435},79,[938,9437,9438],{"class":2690},"                callback_url",[938,9440,1125],{"class":944},[938,9442,9443],{"class":1072},"callback_url",[938,9445,2716],{"class":944},[938,9447,9449],{"class":940,"line":9448},80,[938,9450,5751],{"class":944},[938,9452,9454],{"class":940,"line":9453},81,[938,9455,1057],{"emptyLinePlaceholder":1056},[938,9457,9459,9462,9465,9467,9470],{"class":940,"line":9458},82,[938,9460,9461],{"class":1097},"    except",[938,9463,9464],{"class":1020}," Exception",[938,9466,5543],{"class":1097},[938,9468,9469],{"class":955}," exc",[938,9471,5067],{"class":944},[938,9473,9475,9477,9480,9482,9485,9487,9490,9493,9495,9497,9500],{"class":940,"line":9474},83,[938,9476,5179],{"class":1097},[938,9478,9479],{"class":955}," self",[938,9481,1148],{"class":944},[938,9483,9484],{"class":948},"request",[938,9486,1148],{"class":944},[938,9488,9489],{"class":948},"retries",[938,9491,9492],{"class":944}," >=",[938,9494,9479],{"class":955},[938,9496,1148],{"class":944},[938,9498,9499],{"class":948},"max_retries",[938,9501,5067],{"class":944},[938,9503,9505,9507,9509,9511,9513,9515,9517,9519,9521],{"class":940,"line":9504},84,[938,9506,8835],{"class":955},[938,9508,1148],{"class":944},[938,9510,8862],{"class":948},[938,9512,5214],{"class":944},[938,9514,7624],{"class":955},[938,9516,1148],{"class":944},[938,9518,8065],{"class":948},[938,9520,1148],{"class":944},[938,9522,9523],{"class":948},"FAILED\n",[938,9525,9527,9529,9531,9534,9536,9538,9540,9543],{"class":940,"line":9526},85,[938,9528,8835],{"class":955},[938,9530,1148],{"class":944},[938,9532,9533],{"class":948},"error_message",[938,9535,5214],{"class":944},[938,9537,5457],{"class":1020},[938,9539,1076],{"class":944},[938,9541,9542],{"class":1072},"exc",[938,9544,3256],{"class":944},[938,9546,9548,9550,9552,9554,9556,9558,9560,9562,9564,9566,9568,9570,9572,9574],{"class":940,"line":9547},86,[938,9549,8835],{"class":955},[938,9551,1148],{"class":944},[938,9553,8905],{"class":1072},[938,9555,1076],{"class":944},[938,9557,8910],{"class":2690},[938,9559,8913],{"class":944},[938,9561,2673],{"class":944},[938,9563,8862],{"class":1727},[938,9565,2673],{"class":944},[938,9567,1085],{"class":944},[938,9569,2635],{"class":944},[938,9571,9533],{"class":1727},[938,9573,2673],{"class":944},[938,9575,8938],{"class":944},[938,9577,9579],{"class":940,"line":9578},87,[938,9580,9581],{"class":1097},"            return\n",[938,9583,9585,9588,9590,9593,9595,9597,9599,9601],{"class":940,"line":9584},88,[938,9586,9587],{"class":955},"        self",[938,9589,1148],{"class":944},[938,9591,9592],{"class":1072},"retry",[938,9594,1076],{"class":944},[938,9596,9542],{"class":2690},[938,9598,1125],{"class":944},[938,9600,9542],{"class":1072},[938,9602,3256],{"class":944},[2195,9604,9605],{},[752,9606,3806,9607,9609,9610,9612,9613,9612,9615,9612,9617,9619,9620,9623],{},[920,9608,8977],{}," coordinates (",[920,9611,8341],{},", ",[920,9614,8357],{},[920,9616,8373],{},[920,9618,8389],{},") define where the signature box appears on the PDF. You'll need to calibrate these values for your specific document layout. SignNow uses a coordinate system where ",[920,9621,9622],{},"(0,0)"," is the top-left corner of the page.",[747,9625,9627],{"id":9626},"step-5-handle-webhooks","Step 5: Handle webhooks",[752,9629,9630],{},"When the signer completes the document, SignNow sends a POST request to your webhook URL. We verify the payload signature and dispatch a download task:",[929,9632,9635],{"className":2609,"code":9633,"filename":9634,"language":2611,"meta":934,"style":934},"import hashlib\nimport hmac\n\nfrom rest_framework.permissions import AllowAny\nfrom rest_framework.response import Response\nfrom rest_framework.views import APIView\n\n\nclass SignNowWebhookView(APIView):\n    \"\"\"Receives webhook callbacks from SignNow when documents are signed.\"\"\"\n\n    permission_classes = [AllowAny]\n    authentication_classes = []\n\n    def post(self, request):\n        # Verify webhook signature\n        webhook_secret = getattr(settings, \"SIGNNOW_WEBHOOK_SECRET\", \"\")\n        if webhook_secret:\n            received_signature = request.headers.get(\"X-SignNow-Signature\", \"\")\n            expected = hmac.new(\n                webhook_secret.encode(\"utf-8\"),\n                request.body,\n                hashlib.sha256,\n            ).hexdigest()\n\n            if not hmac.compare_digest(expected, received_signature):\n                return Response({\"status\": \"invalid_signature\"}, status=200)\n\n        # Parse event\n        event = request.data.get(\"event\", \"\")\n        meta = request.data.get(\"meta\", {})\n        document_id = meta.get(\"document_id\", \"\")\n\n        if event in (\"document.complete\", \"document.update\"):\n            signable = SignableDocument.objects.get(\n                signnow_document_id=document_id,\n                status__in=[\"uploaded\", \"invite_sent\"],\n            )\n            signable.status = SignableDocument.Status.SIGNED\n            signable.signed_at = timezone.now()\n            signable.save(update_fields=[\"status\", \"signed_at\"])\n\n            # Download the signed PDF asynchronously\n            download_signed_pdf.delay(signable.id)\n\n        # Always return 200 to acknowledge receipt\n        return Response({\"status\": \"ok\"}, status=200)\n","apps/signnow/api/views.py",[920,9636,9637,9644,9651,9655,9672,9688,9704,9708,9712,9726,9735,9739,9753,9763,9767,9785,9790,9817,9825,9857,9874,9895,9907,9919,9929,9933,9958,9997,10001,10006,10038,10069,10097,10101,10129,10148,10159,10182,10186,10207,10226,10256,10260,10265,10285,10289,10294],{"__ignoreMap":934},[938,9638,9639,9641],{"class":940,"line":941},[938,9640,2618],{"class":1097},[938,9642,9643],{"class":955}," hashlib\n",[938,9645,9646,9648],{"class":940,"line":967},[938,9647,2618],{"class":1097},[938,9649,9650],{"class":955}," hmac\n",[938,9652,9653],{"class":940,"line":1053},[938,9654,1057],{"emptyLinePlaceholder":1056},[938,9656,9657,9659,9662,9664,9667,9669],{"class":940,"line":1060},[938,9658,4984],{"class":1097},[938,9660,9661],{"class":955}," rest_framework",[938,9663,1148],{"class":944},[938,9665,9666],{"class":955},"permissions ",[938,9668,2618],{"class":1097},[938,9670,9671],{"class":955}," AllowAny\n",[938,9673,9674,9676,9678,9680,9683,9685],{"class":940,"line":1066},[938,9675,4984],{"class":1097},[938,9677,9661],{"class":955},[938,9679,1148],{"class":944},[938,9681,9682],{"class":955},"response ",[938,9684,2618],{"class":1097},[938,9686,9687],{"class":955}," Response\n",[938,9689,9690,9692,9694,9696,9699,9701],{"class":940,"line":1094},[938,9691,4984],{"class":1097},[938,9693,9661],{"class":955},[938,9695,1148],{"class":944},[938,9697,9698],{"class":955},"views ",[938,9700,2618],{"class":1097},[938,9702,9703],{"class":955}," APIView\n",[938,9705,9706],{"class":940,"line":1116},[938,9707,1057],{"emptyLinePlaceholder":1056},[938,9709,9710],{"class":940,"line":1134},[938,9711,1057],{"emptyLinePlaceholder":1056},[938,9713,9714,9716,9719,9721,9724],{"class":940,"line":1154},[938,9715,1017],{"class":1035},[938,9717,9718],{"class":1020}," SignNowWebhookView",[938,9720,1076],{"class":944},[938,9722,9723],{"class":1020},"APIView",[938,9725,7637],{"class":944},[938,9727,9728,9730,9733],{"class":940,"line":1171},[938,9729,5072],{"class":1097},[938,9731,9732],{"class":1535},"Receives webhook callbacks from SignNow when documents are signed.",[938,9734,5078],{"class":1097},[938,9736,9737],{"class":940,"line":1191},[938,9738,1057],{"emptyLinePlaceholder":1056},[938,9740,9741,9744,9746,9748,9751],{"class":940,"line":1196},[938,9742,9743],{"class":955},"    permission_classes ",[938,9745,1125],{"class":944},[938,9747,3083],{"class":944},[938,9749,9750],{"class":955},"AllowAny",[938,9752,2767],{"class":944},[938,9754,9755,9758,9760],{"class":940,"line":1231},[938,9756,9757],{"class":955},"    authentication_classes ",[938,9759,1125],{"class":944},[938,9761,9762],{"class":944}," []\n",[938,9764,9765],{"class":940,"line":1237},[938,9766,1057],{"emptyLinePlaceholder":1056},[938,9768,9769,9771,9774,9776,9778,9780,9783],{"class":940,"line":1243},[938,9770,5146],{"class":1035},[938,9772,9773],{"class":1072}," post",[938,9775,1076],{"class":944},[938,9777,8544],{"class":8543},[938,9779,1085],{"class":944},[938,9781,9782],{"class":2690}," request",[938,9784,7637],{"class":944},[938,9786,9787],{"class":940,"line":2431},[938,9788,9789],{"class":1535},"        # Verify webhook signature\n",[938,9791,9792,9795,9797,9799,9801,9803,9805,9807,9809,9811,9813,9815],{"class":940,"line":2437},[938,9793,9794],{"class":955},"        webhook_secret ",[938,9796,1125],{"class":944},[938,9798,5217],{"class":1072},[938,9800,1076],{"class":944},[938,9802,5222],{"class":1072},[938,9804,1085],{"class":944},[938,9806,2635],{"class":944},[938,9808,4713],{"class":1727},[938,9810,2673],{"class":944},[938,9812,1085],{"class":944},[938,9814,5235],{"class":944},[938,9816,3256],{"class":944},[938,9818,9819,9821,9823],{"class":940,"line":2443},[938,9820,5179],{"class":1097},[938,9822,7363],{"class":955},[938,9824,5067],{"class":944},[938,9826,9827,9830,9832,9834,9836,9838,9840,9842,9844,9846,9849,9851,9853,9855],{"class":940,"line":2449},[938,9828,9829],{"class":955},"            received_signature ",[938,9831,1125],{"class":944},[938,9833,9782],{"class":955},[938,9835,1148],{"class":944},[938,9837,2849],{"class":948},[938,9839,1148],{"class":944},[938,9841,5487],{"class":1072},[938,9843,1076],{"class":944},[938,9845,2673],{"class":944},[938,9847,9848],{"class":1727},"X-SignNow-Signature",[938,9850,2673],{"class":944},[938,9852,1085],{"class":944},[938,9854,5235],{"class":944},[938,9856,3256],{"class":944},[938,9858,9859,9862,9864,9867,9869,9872],{"class":940,"line":2454},[938,9860,9861],{"class":955},"            expected ",[938,9863,1125],{"class":944},[938,9865,9866],{"class":955}," hmac",[938,9868,1148],{"class":944},[938,9870,9871],{"class":1072},"new",[938,9873,3210],{"class":944},[938,9875,9876,9879,9881,9884,9886,9888,9891,9893],{"class":940,"line":2460},[938,9877,9878],{"class":1072},"                webhook_secret",[938,9880,1148],{"class":944},[938,9882,9883],{"class":1072},"encode",[938,9885,1076],{"class":944},[938,9887,2673],{"class":944},[938,9889,9890],{"class":1727},"utf-8",[938,9892,2673],{"class":944},[938,9894,6128],{"class":944},[938,9896,9897,9900,9902,9905],{"class":940,"line":2466},[938,9898,9899],{"class":1072},"                request",[938,9901,1148],{"class":944},[938,9903,9904],{"class":948},"body",[938,9906,2716],{"class":944},[938,9908,9909,9912,9914,9917],{"class":940,"line":2472},[938,9910,9911],{"class":1072},"                hashlib",[938,9913,1148],{"class":944},[938,9915,9916],{"class":948},"sha256",[938,9918,2716],{"class":944},[938,9920,9921,9924,9927],{"class":940,"line":3049},[938,9922,9923],{"class":944},"            ).",[938,9925,9926],{"class":1072},"hexdigest",[938,9928,1131],{"class":944},[938,9930,9931],{"class":940,"line":3071},[938,9932,1057],{"emptyLinePlaceholder":1056},[938,9934,9935,9937,9939,9941,9943,9946,9948,9951,9953,9956],{"class":940,"line":3149},[938,9936,8800],{"class":1097},[938,9938,5341],{"class":944},[938,9940,9866],{"class":955},[938,9942,1148],{"class":944},[938,9944,9945],{"class":1072},"compare_digest",[938,9947,1076],{"class":944},[938,9949,9950],{"class":1072},"expected",[938,9952,1085],{"class":944},[938,9954,9955],{"class":1072}," received_signature",[938,9957,7637],{"class":944},[938,9959,9960,9963,9966,9969,9971,9973,9975,9977,9979,9982,9984,9987,9990,9992,9995],{"class":940,"line":3154},[938,9961,9962],{"class":1097},"                return",[938,9964,9965],{"class":1072}," Response",[938,9967,9968],{"class":944},"({",[938,9970,2673],{"class":944},[938,9972,8862],{"class":1727},[938,9974,2673],{"class":944},[938,9976,1880],{"class":944},[938,9978,2635],{"class":944},[938,9980,9981],{"class":1727},"invalid_signature",[938,9983,2673],{"class":944},[938,9985,9986],{"class":944},"},",[938,9988,9989],{"class":2690}," status",[938,9991,1125],{"class":944},[938,9993,9994],{"class":1522},"200",[938,9996,3256],{"class":944},[938,9998,9999],{"class":940,"line":3159},[938,10000,1057],{"emptyLinePlaceholder":1056},[938,10002,10003],{"class":940,"line":3185},[938,10004,10005],{"class":1535},"        # Parse event\n",[938,10007,10008,10011,10013,10015,10017,10020,10022,10024,10026,10028,10030,10032,10034,10036],{"class":940,"line":3190},[938,10009,10010],{"class":955},"        event ",[938,10012,1125],{"class":944},[938,10014,9782],{"class":955},[938,10016,1148],{"class":944},[938,10018,10019],{"class":948},"data",[938,10021,1148],{"class":944},[938,10023,5487],{"class":1072},[938,10025,1076],{"class":944},[938,10027,2673],{"class":944},[938,10029,7231],{"class":1727},[938,10031,2673],{"class":944},[938,10033,1085],{"class":944},[938,10035,5235],{"class":944},[938,10037,3256],{"class":944},[938,10039,10040,10043,10045,10047,10049,10051,10053,10055,10057,10059,10062,10064,10066],{"class":940,"line":3196},[938,10041,10042],{"class":955},"        meta ",[938,10044,1125],{"class":944},[938,10046,9782],{"class":955},[938,10048,1148],{"class":944},[938,10050,10019],{"class":948},[938,10052,1148],{"class":944},[938,10054,5487],{"class":1072},[938,10056,1076],{"class":944},[938,10058,2673],{"class":944},[938,10060,10061],{"class":1727},"meta",[938,10063,2673],{"class":944},[938,10065,1085],{"class":944},[938,10067,10068],{"class":944}," {})\n",[938,10070,10071,10074,10076,10079,10081,10083,10085,10087,10089,10091,10093,10095],{"class":940,"line":3213},[938,10072,10073],{"class":955},"        document_id ",[938,10075,1125],{"class":944},[938,10077,10078],{"class":955}," meta",[938,10080,1148],{"class":944},[938,10082,5487],{"class":1072},[938,10084,1076],{"class":944},[938,10086,2673],{"class":944},[938,10088,6383],{"class":1727},[938,10090,2673],{"class":944},[938,10092,1085],{"class":944},[938,10094,5235],{"class":944},[938,10096,3256],{"class":944},[938,10098,10099],{"class":940,"line":3242},[938,10100,1057],{"emptyLinePlaceholder":1056},[938,10102,10103,10105,10108,10110,10112,10114,10116,10118,10120,10122,10125,10127],{"class":940,"line":3253},[938,10104,5179],{"class":1097},[938,10106,10107],{"class":955}," event ",[938,10109,1110],{"class":944},[938,10111,1101],{"class":944},[938,10113,2673],{"class":944},[938,10115,9412],{"class":1727},[938,10117,2673],{"class":944},[938,10119,1085],{"class":944},[938,10121,2635],{"class":944},[938,10123,10124],{"class":1727},"document.update",[938,10126,2673],{"class":944},[938,10128,7637],{"class":944},[938,10130,10131,10134,10136,10138,10140,10142,10144,10146],{"class":940,"line":3259},[938,10132,10133],{"class":955},"            signable ",[938,10135,1125],{"class":944},[938,10137,7624],{"class":955},[938,10139,1148],{"class":944},[938,10141,8621],{"class":948},[938,10143,1148],{"class":944},[938,10145,5487],{"class":1072},[938,10147,3210],{"class":944},[938,10149,10150,10153,10155,10157],{"class":940,"line":3264},[938,10151,10152],{"class":2690},"                signnow_document_id",[938,10154,1125],{"class":944},[938,10156,6383],{"class":1072},[938,10158,2716],{"class":944},[938,10160,10161,10164,10166,10168,10170,10172,10174,10176,10178,10180],{"class":940,"line":3270},[938,10162,10163],{"class":2690},"                status__in",[938,10165,8913],{"class":944},[938,10167,2673],{"class":944},[938,10169,7704],{"class":1727},[938,10171,2673],{"class":944},[938,10173,1085],{"class":944},[938,10175,2635],{"class":944},[938,10177,7727],{"class":1727},[938,10179,2673],{"class":944},[938,10181,6763],{"class":944},[938,10183,10184],{"class":940,"line":3294},[938,10185,5751],{"class":944},[938,10187,10188,10190,10192,10194,10196,10198,10200,10202,10204],{"class":940,"line":3321},[938,10189,8835],{"class":955},[938,10191,1148],{"class":944},[938,10193,8862],{"class":948},[938,10195,5214],{"class":944},[938,10197,7624],{"class":955},[938,10199,1148],{"class":944},[938,10201,8065],{"class":948},[938,10203,1148],{"class":944},[938,10205,10206],{"class":948},"SIGNED\n",[938,10208,10209,10211,10213,10216,10218,10220,10222,10224],{"class":940,"line":3333},[938,10210,8835],{"class":955},[938,10212,1148],{"class":944},[938,10214,10215],{"class":948},"signed_at",[938,10217,5214],{"class":944},[938,10219,8889],{"class":955},[938,10221,1148],{"class":944},[938,10223,8894],{"class":1072},[938,10225,1131],{"class":944},[938,10227,10228,10230,10232,10234,10236,10238,10240,10242,10244,10246,10248,10250,10252,10254],{"class":940,"line":3356},[938,10229,8835],{"class":955},[938,10231,1148],{"class":944},[938,10233,8905],{"class":1072},[938,10235,1076],{"class":944},[938,10237,8910],{"class":2690},[938,10239,8913],{"class":944},[938,10241,2673],{"class":944},[938,10243,8862],{"class":1727},[938,10245,2673],{"class":944},[938,10247,1085],{"class":944},[938,10249,2635],{"class":944},[938,10251,10215],{"class":1727},[938,10253,2673],{"class":944},[938,10255,8938],{"class":944},[938,10257,10258],{"class":940,"line":5513},[938,10259,1057],{"emptyLinePlaceholder":1056},[938,10261,10262],{"class":940,"line":5518},[938,10263,10264],{"class":1535},"            # Download the signed PDF asynchronously\n",[938,10266,10267,10270,10272,10275,10277,10279,10281,10283],{"class":940,"line":5551},[938,10268,10269],{"class":955},"            download_signed_pdf",[938,10271,1148],{"class":944},[938,10273,10274],{"class":1072},"delay",[938,10276,1076],{"class":944},[938,10278,9011],{"class":1072},[938,10280,1148],{"class":944},[938,10282,3178],{"class":948},[938,10284,3256],{"class":944},[938,10286,10287],{"class":940,"line":5567},[938,10288,1057],{"emptyLinePlaceholder":1056},[938,10290,10291],{"class":940,"line":5590},[938,10292,10293],{"class":1535},"        # Always return 200 to acknowledge receipt\n",[938,10295,10296,10298,10300,10302,10304,10306,10308,10310,10312,10315,10317,10319,10321,10323,10325],{"class":940,"line":5598},[938,10297,5427],{"class":1097},[938,10299,9965],{"class":1072},[938,10301,9968],{"class":944},[938,10303,2673],{"class":944},[938,10305,8862],{"class":1727},[938,10307,2673],{"class":944},[938,10309,1880],{"class":944},[938,10311,2635],{"class":944},[938,10313,10314],{"class":1727},"ok",[938,10316,2673],{"class":944},[938,10318,9986],{"class":944},[938,10320,9989],{"class":2690},[938,10322,1125],{"class":944},[938,10324,9994],{"class":1522},[938,10326,3256],{"class":944},[2204,10328,10329],{},[752,10330,10331],{},"Always return HTTP 200 from your webhook endpoint, even when processing fails. SignNow will retry delivery on non-2xx responses, which can lead to duplicate processing if your error is in business logic rather than network issues.",[752,10333,10334],{},"Wire it up in your URL configuration:",[929,10336,10339],{"className":2609,"code":10337,"filename":10338,"language":2611,"meta":934,"style":934},"from django.urls import path\nfrom signnow.api.views import SignNowWebhookView\n\nurlpatterns = [\n    path(\"webhooks/\", SignNowWebhookView.as_view(), name=\"signnow-webhook\"),\n]\n","apps/signnow/api/urls.py",[920,10340,10341,10357,10377,10381,10390,10430],{"__ignoreMap":934},[938,10342,10343,10345,10347,10349,10352,10354],{"class":940,"line":941},[938,10344,4984],{"class":1097},[938,10346,4987],{"class":955},[938,10348,1148],{"class":944},[938,10350,10351],{"class":955},"urls ",[938,10353,2618],{"class":1097},[938,10355,10356],{"class":955}," path\n",[938,10358,10359,10361,10363,10365,10368,10370,10372,10374],{"class":940,"line":967},[938,10360,4984],{"class":1097},[938,10362,8573],{"class":955},[938,10364,1148],{"class":944},[938,10366,10367],{"class":955},"api",[938,10369,1148],{"class":944},[938,10371,9698],{"class":955},[938,10373,2618],{"class":1097},[938,10375,10376],{"class":955}," SignNowWebhookView\n",[938,10378,10379],{"class":940,"line":1053},[938,10380,1057],{"emptyLinePlaceholder":1056},[938,10382,10383,10386,10388],{"class":940,"line":1060},[938,10384,10385],{"class":955},"urlpatterns ",[938,10387,1125],{"class":944},[938,10389,2909],{"class":944},[938,10391,10392,10395,10397,10399,10402,10404,10406,10408,10410,10413,10416,10419,10421,10423,10426,10428],{"class":940,"line":1066},[938,10393,10394],{"class":1072},"    path",[938,10396,1076],{"class":944},[938,10398,2673],{"class":944},[938,10400,10401],{"class":1727},"webhooks/",[938,10403,2673],{"class":944},[938,10405,1085],{"class":944},[938,10407,9718],{"class":1072},[938,10409,1148],{"class":944},[938,10411,10412],{"class":1072},"as_view",[938,10414,10415],{"class":944},"(),",[938,10417,10418],{"class":2690}," name",[938,10420,1125],{"class":944},[938,10422,2673],{"class":944},[938,10424,10425],{"class":1727},"signnow-webhook",[938,10427,2673],{"class":944},[938,10429,6128],{"class":944},[938,10431,10432],{"class":940,"line":1094},[938,10433,2767],{"class":944},[752,10435,10436],{},"And include it in your main API URLs:",[929,10438,10441],{"className":2609,"code":10439,"filename":10440,"language":2611,"meta":934,"style":934},"urlpatterns = [\n    # ... other routes\n    path(\"v1/signnow/\", include(\"signnow.api.urls\")),\n]\n","apps/api/urls.py",[920,10442,10443,10451,10456,10486],{"__ignoreMap":934},[938,10444,10445,10447,10449],{"class":940,"line":941},[938,10446,10385],{"class":955},[938,10448,1125],{"class":944},[938,10450,2909],{"class":944},[938,10452,10453],{"class":940,"line":967},[938,10454,10455],{"class":1535},"    # ... other routes\n",[938,10457,10458,10460,10462,10464,10467,10469,10471,10474,10476,10478,10481,10483],{"class":940,"line":1053},[938,10459,10394],{"class":1072},[938,10461,1076],{"class":944},[938,10463,2673],{"class":944},[938,10465,10466],{"class":1727},"v1/signnow/",[938,10468,2673],{"class":944},[938,10470,1085],{"class":944},[938,10472,10473],{"class":1072}," include",[938,10475,1076],{"class":944},[938,10477,2673],{"class":944},[938,10479,10480],{"class":1727},"signnow.api.urls",[938,10482,2673],{"class":944},[938,10484,10485],{"class":944},")),\n",[938,10487,10488],{"class":940,"line":1060},[938,10489,2767],{"class":944},[747,10491,10493],{"id":10492},"step-6-download-the-signed-pdf","Step 6: Download the signed PDF",[752,10495,10496],{},"The second Celery task fetches the completed, signed document from SignNow and saves it locally:",[929,10498,10500],{"className":2609,"code":10499,"filename":8302,"language":2611,"meta":934,"style":934},"@shared_task(bind=True, max_retries=5, default_retry_delay=120)\ndef download_signed_pdf(self, signable_document_id: int):\n    \"\"\"Download the signed PDF from SignNow and save it locally.\"\"\"\n    from signnow.models import SignableDocument\n    from signnow.services.signnow_service import SignNowService\n\n    signable = SignableDocument.objects.get(id=signable_document_id)\n\n    # Skip if already downloaded (idempotent)\n    if signable.status == SignableDocument.Status.DOWNLOADED:\n        return\n\n    pdf_bytes = SignNowService.download_signed_document(signable.signnow_document_id)\n    if not pdf_bytes:\n        raise RuntimeError(\"Failed to download signed document\")\n\n    signed_filename = f\"signed-{signable.original_pdf_name.split('/')[-1]}\"\n    signable.signed_pdf.save(signed_filename, ContentFile(pdf_bytes), save=False)\n    signable.status = SignableDocument.Status.DOWNLOADED\n    signable.downloaded_at = timezone.now()\n    signable.save(update_fields=[\"signed_pdf\", \"status\", \"downloaded_at\"])\n",[920,10501,10502,10532,10553,10562,10576,10594,10598,10625,10629,10634,10659,10664,10668,10692,10702,10720,10724,10768,10804,10825,10844],{"__ignoreMap":934},[938,10503,10504,10506,10508,10510,10512,10514,10516,10518,10521,10523,10525,10527,10530],{"class":940,"line":941},[938,10505,5951],{"class":944},[938,10507,8504],{"class":1072},[938,10509,1076],{"class":944},[938,10511,8509],{"class":2690},[938,10513,7938],{"class":944},[938,10515,8514],{"class":2690},[938,10517,1125],{"class":944},[938,10519,10520],{"class":1522},"5",[938,10522,1085],{"class":944},[938,10524,8524],{"class":2690},[938,10526,1125],{"class":944},[938,10528,10529],{"class":1522},"120",[938,10531,3256],{"class":944},[938,10533,10534,10536,10539,10541,10543,10545,10547,10549,10551],{"class":940,"line":967},[938,10535,5958],{"class":1035},[938,10537,10538],{"class":1072}," download_signed_pdf",[938,10540,1076],{"class":944},[938,10542,8544],{"class":8543},[938,10544,1085],{"class":944},[938,10546,8549],{"class":2690},[938,10548,1880],{"class":944},[938,10550,8554],{"class":1020},[938,10552,7637],{"class":944},[938,10554,10555,10557,10560],{"class":940,"line":1053},[938,10556,5072],{"class":1097},[938,10558,10559],{"class":1535},"Download the signed PDF from SignNow and save it locally.",[938,10561,5078],{"class":1097},[938,10563,10564,10566,10568,10570,10572,10574],{"class":940,"line":1060},[938,10565,8570],{"class":1097},[938,10567,8573],{"class":955},[938,10569,1148],{"class":944},[938,10571,7588],{"class":955},[938,10573,2618],{"class":1097},[938,10575,8582],{"class":955},[938,10577,10578,10580,10582,10584,10586,10588,10590,10592],{"class":940,"line":1066},[938,10579,8570],{"class":1097},[938,10581,8573],{"class":955},[938,10583,1148],{"class":944},[938,10585,8593],{"class":955},[938,10587,1148],{"class":944},[938,10589,8598],{"class":955},[938,10591,2618],{"class":1097},[938,10593,8603],{"class":955},[938,10595,10596],{"class":940,"line":1094},[938,10597,1057],{"emptyLinePlaceholder":1056},[938,10599,10600,10602,10604,10606,10608,10610,10612,10614,10616,10618,10620,10623],{"class":940,"line":1116},[938,10601,8612],{"class":955},[938,10603,1125],{"class":944},[938,10605,7624],{"class":955},[938,10607,1148],{"class":944},[938,10609,8621],{"class":948},[938,10611,1148],{"class":944},[938,10613,5487],{"class":1072},[938,10615,1076],{"class":944},[938,10617,3178],{"class":2690},[938,10619,1125],{"class":944},[938,10621,10622],{"class":1072},"signable_document_id",[938,10624,3256],{"class":944},[938,10626,10627],{"class":940,"line":1134},[938,10628,1057],{"emptyLinePlaceholder":1056},[938,10630,10631],{"class":940,"line":1154},[938,10632,10633],{"class":1535},"    # Skip if already downloaded (idempotent)\n",[938,10635,10636,10638,10640,10642,10644,10646,10648,10650,10652,10654,10657],{"class":940,"line":1171},[938,10637,6025],{"class":1097},[938,10639,8681],{"class":955},[938,10641,1148],{"class":944},[938,10643,8862],{"class":948},[938,10645,9093],{"class":944},[938,10647,7624],{"class":955},[938,10649,1148],{"class":944},[938,10651,8065],{"class":948},[938,10653,1148],{"class":944},[938,10655,10656],{"class":948},"DOWNLOADED",[938,10658,5067],{"class":944},[938,10660,10661],{"class":940,"line":1191},[938,10662,10663],{"class":1097},"        return\n",[938,10665,10666],{"class":940,"line":1196},[938,10667,1057],{"emptyLinePlaceholder":1056},[938,10669,10670,10673,10675,10677,10679,10682,10684,10686,10688,10690],{"class":940,"line":1231},[938,10671,10672],{"class":955},"    pdf_bytes ",[938,10674,1125],{"class":944},[938,10676,5064],{"class":955},[938,10678,1148],{"class":944},[938,10680,10681],{"class":1072},"download_signed_document",[938,10683,1076],{"class":944},[938,10685,9011],{"class":1072},[938,10687,1148],{"class":944},[938,10689,8768],{"class":948},[938,10691,3256],{"class":944},[938,10693,10694,10696,10698,10700],{"class":940,"line":1237},[938,10695,6025],{"class":1097},[938,10697,5341],{"class":944},[938,10699,5970],{"class":955},[938,10701,5067],{"class":944},[938,10703,10704,10707,10709,10711,10713,10716,10718],{"class":940,"line":1243},[938,10705,10706],{"class":1097},"        raise",[938,10708,8815],{"class":1020},[938,10710,1076],{"class":944},[938,10712,2673],{"class":944},[938,10714,10715],{"class":1727},"Failed to download signed document",[938,10717,2673],{"class":944},[938,10719,3256],{"class":944},[938,10721,10722],{"class":940,"line":2431},[938,10723,1057],{"emptyLinePlaceholder":1056},[938,10725,10726,10729,10731,10733,10736,10738,10740,10742,10745,10747,10749,10751,10754,10756,10758,10760,10762,10764,10766],{"class":940,"line":2437},[938,10727,10728],{"class":955},"    signed_filename ",[938,10730,1125],{"class":944},[938,10732,2789],{"class":1035},[938,10734,10735],{"class":1727},"\"signed-",[938,10737,2676],{"class":1522},[938,10739,9011],{"class":955},[938,10741,1148],{"class":944},[938,10743,10744],{"class":948},"original_pdf_name",[938,10746,1148],{"class":944},[938,10748,8730],{"class":1072},[938,10750,1076],{"class":944},[938,10752,10753],{"class":944},"'",[938,10755,1494],{"class":1727},[938,10757,10753],{"class":944},[938,10759,8741],{"class":944},[938,10761,8744],{"class":1522},[938,10763,6931],{"class":944},[938,10765,2682],{"class":1522},[938,10767,2641],{"class":1727},[938,10769,10770,10773,10775,10778,10780,10782,10784,10787,10789,10792,10794,10796,10798,10801],{"class":940,"line":2443},[938,10771,10772],{"class":955},"    signable",[938,10774,1148],{"class":944},[938,10776,10777],{"class":948},"signed_pdf",[938,10779,1148],{"class":944},[938,10781,8905],{"class":1072},[938,10783,1076],{"class":944},[938,10785,10786],{"class":1072},"signed_filename",[938,10788,1085],{"class":944},[938,10790,10791],{"class":1072}," ContentFile",[938,10793,1076],{"class":944},[938,10795,8789],{"class":1072},[938,10797,6409],{"class":944},[938,10799,10800],{"class":2690}," save",[938,10802,10803],{"class":944},"=False)\n",[938,10805,10806,10808,10810,10812,10814,10816,10818,10820,10822],{"class":940,"line":2449},[938,10807,10772],{"class":955},[938,10809,1148],{"class":944},[938,10811,8862],{"class":948},[938,10813,5214],{"class":944},[938,10815,7624],{"class":955},[938,10817,1148],{"class":944},[938,10819,8065],{"class":948},[938,10821,1148],{"class":944},[938,10823,10824],{"class":948},"DOWNLOADED\n",[938,10826,10827,10829,10831,10834,10836,10838,10840,10842],{"class":940,"line":2454},[938,10828,10772],{"class":955},[938,10830,1148],{"class":944},[938,10832,10833],{"class":948},"downloaded_at",[938,10835,5214],{"class":944},[938,10837,8889],{"class":955},[938,10839,1148],{"class":944},[938,10841,8894],{"class":1072},[938,10843,1131],{"class":944},[938,10845,10846,10848,10850,10852,10854,10856,10858,10860,10862,10864,10866,10868,10870,10872,10874,10876,10878,10880],{"class":940,"line":2460},[938,10847,10772],{"class":955},[938,10849,1148],{"class":944},[938,10851,8905],{"class":1072},[938,10853,1076],{"class":944},[938,10855,8910],{"class":2690},[938,10857,8913],{"class":944},[938,10859,2673],{"class":944},[938,10861,10777],{"class":1727},[938,10863,2673],{"class":944},[938,10865,1085],{"class":944},[938,10867,2635],{"class":944},[938,10869,8862],{"class":1727},[938,10871,2673],{"class":944},[938,10873,1085],{"class":944},[938,10875,2635],{"class":944},[938,10877,10833],{"class":1727},[938,10879,2673],{"class":944},[938,10881,8938],{"class":944},[747,10883,10885],{"id":10884},"step-7-admin-integration","Step 7: Admin integration",[752,10887,10888],{},"Finally, we added a read-only admin panel with color-coded status badges so our team can monitor signing progress at a glance:",[929,10890,10893],{"className":2609,"code":10891,"filename":10892,"language":2611,"meta":934,"style":934},"from django.contrib import admin\nfrom django.utils.html import format_html\n\n\n@admin.register(SignableDocument)\nclass SignableDocumentAdmin(admin.ModelAdmin):\n    list_display = [\"signer_name\", \"signer_email\", \"status_badge\", \"created_at\", \"signed_at\"]\n    list_filter = [\"status\", \"created_at\"]\n    search_fields = [\"signer_email\", \"signer_name\", \"signnow_document_id\"]\n\n    @admin.display(description=\"Status\")\n    def status_badge(self, obj):\n        colors = {\n            \"pending\": \"#6b7280\",\n            \"uploaded\": \"#3b82f6\",\n            \"invite_sent\": \"#f59e0b\",\n            \"signed\": \"#22c55e\",\n            \"downloaded\": \"#059669\",\n            \"failed\": \"#ef4444\",\n        }\n        color = colors.get(obj.status, \"#6b7280\")\n        return format_html(\n            '\u003Cspan style=\"background-color: {}; color: white; padding: 2px 8px; '\n            'border-radius: 4px; font-size: 12px;\">{}\u003C/span>',\n            color,\n            obj.get_status_display(),\n        )\n","apps/signnow/admin.py",[920,10894,10895,10911,10932,10936,10940,10958,10976,11028,11053,11086,11090,11116,11134,11143,11162,11181,11200,11219,11238,11257,11262,11295,11304,11321,11337,11344,11357],{"__ignoreMap":934},[938,10896,10897,10899,10901,10903,10906,10908],{"class":940,"line":941},[938,10898,4984],{"class":1097},[938,10900,4987],{"class":955},[938,10902,1148],{"class":944},[938,10904,10905],{"class":955},"contrib ",[938,10907,2618],{"class":1097},[938,10909,10910],{"class":955}," admin\n",[938,10912,10913,10915,10917,10919,10922,10924,10927,10929],{"class":940,"line":967},[938,10914,4984],{"class":1097},[938,10916,4987],{"class":955},[938,10918,1148],{"class":944},[938,10920,10921],{"class":955},"utils",[938,10923,1148],{"class":944},[938,10925,10926],{"class":955},"html ",[938,10928,2618],{"class":1097},[938,10930,10931],{"class":955}," format_html\n",[938,10933,10934],{"class":940,"line":1053},[938,10935,1057],{"emptyLinePlaceholder":1056},[938,10937,10938],{"class":940,"line":1060},[938,10939,1057],{"emptyLinePlaceholder":1056},[938,10941,10942,10944,10947,10949,10952,10954,10956],{"class":940,"line":1066},[938,10943,5951],{"class":944},[938,10945,10946],{"class":1072},"admin",[938,10948,1148],{"class":944},[938,10950,10951],{"class":1072},"register",[938,10953,1076],{"class":944},[938,10955,4594],{"class":1072},[938,10957,3256],{"class":944},[938,10959,10960,10962,10965,10967,10969,10971,10974],{"class":940,"line":1094},[938,10961,1017],{"class":1035},[938,10963,10964],{"class":1020}," SignableDocumentAdmin",[938,10966,1076],{"class":944},[938,10968,10946],{"class":1020},[938,10970,1148],{"class":944},[938,10972,10973],{"class":1020},"ModelAdmin",[938,10975,7637],{"class":944},[938,10977,10978,10981,10983,10985,10987,10990,10992,10994,10996,10998,11000,11002,11004,11007,11009,11011,11013,11016,11018,11020,11022,11024,11026],{"class":940,"line":1116},[938,10979,10980],{"class":955},"    list_display ",[938,10982,1125],{"class":944},[938,10984,3083],{"class":944},[938,10986,2673],{"class":944},[938,10988,10989],{"class":1727},"signer_name",[938,10991,2673],{"class":944},[938,10993,1085],{"class":944},[938,10995,2635],{"class":944},[938,10997,9137],{"class":1727},[938,10999,2673],{"class":944},[938,11001,1085],{"class":944},[938,11003,2635],{"class":944},[938,11005,11006],{"class":1727},"status_badge",[938,11008,2673],{"class":944},[938,11010,1085],{"class":944},[938,11012,2635],{"class":944},[938,11014,11015],{"class":1727},"created_at",[938,11017,2673],{"class":944},[938,11019,1085],{"class":944},[938,11021,2635],{"class":944},[938,11023,10215],{"class":1727},[938,11025,2673],{"class":944},[938,11027,2767],{"class":944},[938,11029,11030,11033,11035,11037,11039,11041,11043,11045,11047,11049,11051],{"class":940,"line":1134},[938,11031,11032],{"class":955},"    list_filter ",[938,11034,1125],{"class":944},[938,11036,3083],{"class":944},[938,11038,2673],{"class":944},[938,11040,8862],{"class":1727},[938,11042,2673],{"class":944},[938,11044,1085],{"class":944},[938,11046,2635],{"class":944},[938,11048,11015],{"class":1727},[938,11050,2673],{"class":944},[938,11052,2767],{"class":944},[938,11054,11055,11058,11060,11062,11064,11066,11068,11070,11072,11074,11076,11078,11080,11082,11084],{"class":940,"line":1154},[938,11056,11057],{"class":955},"    search_fields ",[938,11059,1125],{"class":944},[938,11061,3083],{"class":944},[938,11063,2673],{"class":944},[938,11065,9137],{"class":1727},[938,11067,2673],{"class":944},[938,11069,1085],{"class":944},[938,11071,2635],{"class":944},[938,11073,10989],{"class":1727},[938,11075,2673],{"class":944},[938,11077,1085],{"class":944},[938,11079,2635],{"class":944},[938,11081,8768],{"class":1727},[938,11083,2673],{"class":944},[938,11085,2767],{"class":944},[938,11087,11088],{"class":940,"line":1171},[938,11089,1057],{"emptyLinePlaceholder":1056},[938,11091,11092,11094,11096,11098,11101,11103,11106,11108,11110,11112,11114],{"class":940,"line":1191},[938,11093,5138],{"class":944},[938,11095,10946],{"class":1072},[938,11097,1148],{"class":944},[938,11099,11100],{"class":1072},"display",[938,11102,1076],{"class":944},[938,11104,11105],{"class":2690},"description",[938,11107,1125],{"class":944},[938,11109,2673],{"class":944},[938,11111,8065],{"class":1727},[938,11113,2673],{"class":944},[938,11115,3256],{"class":944},[938,11117,11118,11120,11123,11125,11127,11129,11132],{"class":940,"line":1196},[938,11119,5146],{"class":1035},[938,11121,11122],{"class":1072}," status_badge",[938,11124,1076],{"class":944},[938,11126,8544],{"class":8543},[938,11128,1085],{"class":944},[938,11130,11131],{"class":2690}," obj",[938,11133,7637],{"class":944},[938,11135,11136,11139,11141],{"class":940,"line":1231},[938,11137,11138],{"class":955},"        colors ",[938,11140,1125],{"class":944},[938,11142,1030],{"class":944},[938,11144,11145,11147,11149,11151,11153,11155,11158,11160],{"class":940,"line":1237},[938,11146,7292],{"class":944},[938,11148,7681],{"class":1727},[938,11150,2673],{"class":944},[938,11152,1880],{"class":944},[938,11154,2635],{"class":944},[938,11156,11157],{"class":1727},"#6b7280",[938,11159,2673],{"class":944},[938,11161,2716],{"class":944},[938,11163,11164,11166,11168,11170,11172,11174,11177,11179],{"class":940,"line":1243},[938,11165,7292],{"class":944},[938,11167,7704],{"class":1727},[938,11169,2673],{"class":944},[938,11171,1880],{"class":944},[938,11173,2635],{"class":944},[938,11175,11176],{"class":1727},"#3b82f6",[938,11178,2673],{"class":944},[938,11180,2716],{"class":944},[938,11182,11183,11185,11187,11189,11191,11193,11196,11198],{"class":940,"line":2431},[938,11184,7292],{"class":944},[938,11186,7727],{"class":1727},[938,11188,2673],{"class":944},[938,11190,1880],{"class":944},[938,11192,2635],{"class":944},[938,11194,11195],{"class":1727},"#f59e0b",[938,11197,2673],{"class":944},[938,11199,2716],{"class":944},[938,11201,11202,11204,11206,11208,11210,11212,11215,11217],{"class":940,"line":2437},[938,11203,7292],{"class":944},[938,11205,7750],{"class":1727},[938,11207,2673],{"class":944},[938,11209,1880],{"class":944},[938,11211,2635],{"class":944},[938,11213,11214],{"class":1727},"#22c55e",[938,11216,2673],{"class":944},[938,11218,2716],{"class":944},[938,11220,11221,11223,11225,11227,11229,11231,11234,11236],{"class":940,"line":2443},[938,11222,7292],{"class":944},[938,11224,7773],{"class":1727},[938,11226,2673],{"class":944},[938,11228,1880],{"class":944},[938,11230,2635],{"class":944},[938,11232,11233],{"class":1727},"#059669",[938,11235,2673],{"class":944},[938,11237,2716],{"class":944},[938,11239,11240,11242,11244,11246,11248,11250,11253,11255],{"class":940,"line":2449},[938,11241,7292],{"class":944},[938,11243,7796],{"class":1727},[938,11245,2673],{"class":944},[938,11247,1880],{"class":944},[938,11249,2635],{"class":944},[938,11251,11252],{"class":1727},"#ef4444",[938,11254,2673],{"class":944},[938,11256,2716],{"class":944},[938,11258,11259],{"class":940,"line":2454},[938,11260,11261],{"class":944},"        }\n",[938,11263,11264,11267,11269,11272,11274,11276,11278,11281,11283,11285,11287,11289,11291,11293],{"class":940,"line":2460},[938,11265,11266],{"class":955},"        color ",[938,11268,1125],{"class":944},[938,11270,11271],{"class":955}," colors",[938,11273,1148],{"class":944},[938,11275,5487],{"class":1072},[938,11277,1076],{"class":944},[938,11279,11280],{"class":1072},"obj",[938,11282,1148],{"class":944},[938,11284,8862],{"class":948},[938,11286,1085],{"class":944},[938,11288,2635],{"class":944},[938,11290,11157],{"class":1727},[938,11292,2673],{"class":944},[938,11294,3256],{"class":944},[938,11296,11297,11299,11302],{"class":940,"line":2466},[938,11298,5427],{"class":1097},[938,11300,11301],{"class":1072}," format_html",[938,11303,3210],{"class":944},[938,11305,11306,11309,11312,11315,11318],{"class":940,"line":2472},[938,11307,11308],{"class":944},"            '",[938,11310,11311],{"class":1727},"\u003Cspan style=\"background-color: ",[938,11313,11314],{"class":1522},"{}",[938,11316,11317],{"class":1727},"; color: white; padding: 2px 8px; ",[938,11319,11320],{"class":944},"'\n",[938,11322,11323,11325,11328,11330,11333,11335],{"class":940,"line":3049},[938,11324,11308],{"class":944},[938,11326,11327],{"class":1727},"border-radius: 4px; font-size: 12px;\">",[938,11329,11314],{"class":1522},[938,11331,11332],{"class":1727},"\u003C/span>",[938,11334,10753],{"class":944},[938,11336,2716],{"class":944},[938,11338,11339,11342],{"class":940,"line":3071},[938,11340,11341],{"class":1072},"            color",[938,11343,2716],{"class":944},[938,11345,11346,11349,11351,11354],{"class":940,"line":3149},[938,11347,11348],{"class":1072},"            obj",[938,11350,1148],{"class":944},[938,11352,11353],{"class":1072},"get_status_display",[938,11355,11356],{"class":944},"(),\n",[938,11358,11359],{"class":940,"line":3154},[938,11360,6170],{"class":944},[747,11362,11364],{"id":11363},"lessons-learned","Lessons learned",[752,11366,11367],{},"After running this in production, here are the things we wish we'd known earlier:",[774,11369,11371],{"id":11370},"_1-role-based-invites-require-a-two-step-process","1. Role-based invites require a two-step process",[752,11373,11374,11375,11378,11379,11381],{},"You can't send a role-based invite immediately after adding fields. You need to ",[755,11376,11377],{},"re-fetch the document"," to get the ",[920,11380,9164],{}," that SignNow assigns when processing the fields. This caught us off guard because freeform invites don't have this requirement.",[774,11383,11385],{"id":11384},"_2-token-caching-is-essential","2. Token caching is essential",[752,11387,11388],{},"SignNow's OAuth2 tokens last about an hour. Without caching, every API call would require a round-trip to the token endpoint first. We use Django's cache framework (backed by Redis) to store tokens with a 5-minute safety buffer before expiry.",[774,11390,11392],{"id":11391},"_3-webhook-signatures-use-hmac-sha256","3. Webhook signatures use HMAC-SHA256",[752,11394,11395,11396,11398,11399,11402],{},"Always verify webhook payloads in production. SignNow sends a ",[920,11397,9848],{}," header containing an HMAC-SHA256 digest of the request body. Use ",[920,11400,11401],{},"hmac.compare_digest()"," for timing-safe comparison.",[774,11404,11406],{"id":11405},"_4-custom-invite-messages-require-a-paid-plan","4. Custom invite messages require a paid plan",[752,11408,11409],{},"If you're using the free sandbox, you can send invites, but custom subjects and messages are a paid feature. The default invite email from SignNow is generic but functional for testing.",[774,11411,11413],{"id":11412},"_5-signature-field-coordinates-need-calibration","5. Signature field coordinates need calibration",[752,11415,3806,11416,9612,11418,9612,11420,11422,11423,11425],{},[920,11417,8341],{},[920,11419,8357],{},[920,11421,8373],{},", and ",[920,11424,8389],{}," values for signature fields depend entirely on your PDF layout. We recommend uploading a test document to SignNow's web UI, manually placing a signature field, and then inspecting the document's JSON to capture the exact coordinates.",[747,11427,1901],{"id":1900},[752,11429,11430],{},"Here's the complete API flow at a glance:",[1906,11432,11433,11449],{},[1909,11434,11435],{},[1912,11436,11437,11440,11443,11446],{},[1915,11438,11439],{},"Step",[1915,11441,11442],{},"API Endpoint",[1915,11444,11445],{},"Method",[1915,11447,11448],{},"Purpose",[1922,11450,11451,11466,11481,11496,11511,11525,11540],{},[1912,11452,11453,11455,11460,11463],{},[1927,11454,8744],{},[1927,11456,11457],{},[920,11458,11459],{},"/oauth2/token",[1927,11461,11462],{},"POST",[1927,11464,11465],{},"Get access token",[1912,11467,11468,11471,11476,11478],{},[1927,11469,11470],{},"2",[1927,11472,11473],{},[920,11474,11475],{},"/document",[1927,11477,11462],{},[1927,11479,11480],{},"Upload PDF",[1912,11482,11483,11485,11490,11493],{},[1927,11484,8519],{},[1927,11486,11487],{},[920,11488,11489],{},"/document/{id}",[1927,11491,11492],{},"PUT",[1927,11494,11495],{},"Add signature fields",[1912,11497,11498,11501,11505,11508],{},[1927,11499,11500],{},"4",[1927,11502,11503],{},[920,11504,11489],{},[1927,11506,11507],{},"GET",[1927,11509,11510],{},"Retrieve role IDs",[1912,11512,11513,11515,11520,11522],{},[1927,11514,10520],{},[1927,11516,11517],{},[920,11518,11519],{},"/document/{id}/invite",[1927,11521,11462],{},[1927,11523,11524],{},"Send signing invite",[1912,11526,11527,11530,11535,11537],{},[1927,11528,11529],{},"6",[1927,11531,11532],{},[920,11533,11534],{},"/v2/events",[1927,11536,11462],{},[1927,11538,11539],{},"Register webhook",[1912,11541,11542,11545,11550,11552],{},[1927,11543,11544],{},"7",[1927,11546,11547],{},[920,11548,11549],{},"/document/{id}/download",[1927,11551,11507],{},[1927,11553,11554],{},"Download signed PDF",[752,11556,11557,11558,11561],{},"The full integration is about ",[755,11559,11560],{},"400 lines of Python"," across four files - a service layer, a model, two Celery tasks, and a webhook view. It plugs cleanly into any Django project that needs e-signature capabilities.",[2043,11563,11564],{},"html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .s5tWE, html code.shiki .s5tWE{--shiki-light:#E53935;--shiki-light-font-style:italic;--shiki-default:#F07178;--shiki-default-font-style:italic;--shiki-dark:#F07178;--shiki-dark-font-style:italic}",{"title":934,"searchDepth":967,"depth":967,"links":11566},[11567,11568,11569,11570,11576,11577,11578,11579,11580,11581,11588],{"id":4456,"depth":967,"text":4457},{"id":4540,"depth":967,"text":4541},{"id":4617,"depth":967,"text":4618},{"id":4953,"depth":967,"text":4954,"children":11571},[11572,11573,11574,11575],{"id":4963,"depth":1053,"text":4964},{"id":5937,"depth":1053,"text":5938},{"id":6688,"depth":1053,"text":6689},{"id":7112,"depth":1053,"text":7113},{"id":7526,"depth":967,"text":7527},{"id":8294,"depth":967,"text":8295},{"id":9626,"depth":967,"text":9627},{"id":10492,"depth":967,"text":10493},{"id":10884,"depth":967,"text":10885},{"id":11363,"depth":967,"text":11364,"children":11582},[11583,11584,11585,11586,11587],{"id":11370,"depth":1053,"text":11371},{"id":11384,"depth":1053,"text":11385},{"id":11391,"depth":1053,"text":11392},{"id":11405,"depth":1053,"text":11406},{"id":11412,"depth":1053,"text":11413},{"id":1900,"depth":967,"text":1901},"software-development","2026-02-13T00:00:00.000Z","A practical guide to adding e-signature capabilities to your Django app using the SignNow REST API. We walk through the full integration - from OAuth2 authentication and document uploads to asynchronous signing workflows with Celery and real-time webhook handling - based on our production implementation at BeatBuddy.",{"src":11593,"credit":11594},"/images/blog/musictechlab_blog_signnow_django_integration.webp","Photo by [Scott Graham](https://unsplash.com/@amstram) on [Unsplash](https://unsplash.com/photos/OQMZwNd3ThU)",{"enabled":1056,"items":11596},[11597,11599,11601,11604],{"text":11598,"icon":4407},"The full SignNow Django integration is about 400 lines across four files.",{"text":11600,"icon":2081},"All SignNow API calls run in Celery tasks to keep the request cycle fast.",{"text":11602,"icon":11603},"Role-based invites need a two-step process: add fields, then re-fetch to get role IDs.","i-lucide-git-branch",{"text":11605,"icon":11606},"Cache OAuth2 tokens in Redis with a 5-minute buffer to avoid extra round-trips.","i-lucide-clock",{},{"title":558,"description":11591},[2094],"B2omIw3TRWUTJSZukF0UtP4lbpEdNuc7Vc86WdkLOqQ",{"id":11612,"title":116,"authors":11613,"badge":11616,"body":11619,"category":4398,"client":742,"date":13482,"description":13483,"extension":2074,"faq":742,"featured":69,"featuredOrder":742,"hidden":69,"image":13484,"keyTakeaways":13486,"meta":13499,"navigation":1056,"path":117,"seo":13500,"status":742,"stem":118,"tags":13501,"teaser":742,"__hash__":13504,"score":941},"posts/blog/music-data/building-a-suno-remix-app-with-nuxt-and-firebase.md",[11614],{"name":738,"to":739,"avatar":11615},{"src":741},{"label":11617,"color":11618},"AI Audio","#7c3aed",{"type":744,"value":11620,"toc":13462},[11621,11633,11640,11642,11646,11654,11665,11684,11689,11691,11695,11702,11709,11747,11752,11805,11807,11811,11893,11895,11899,11902,11906,11913,12358,12364,12368,12371,12704,12724,12728,12731,12878,12885,12887,12891,12894,12920,12923,12930,12937,12943,12946,12952,12956,12963,13008,13011,13013,13017,13020,13180,13183,13189,13192,13213,13215,13219,13222,13293,13295,13299,13306,13312,13319,13328,13331,13333,13337,13369,13371,13375,13378,13410,13412,13416,13429,13436,13438,13442,13459],[752,11622,11623,11624,9612,11627,11422,11630,1148],{},"Imagine uploading a raw audio recording and getting back a fully remixed version in a completely different genre - Pop to Jazz, Rock to Lo-fi - all powered by AI. That's exactly what we built as a proof of concept using ",[755,11625,11626],{},"Suno's AI",[755,11628,11629],{},"Nuxt 4",[755,11631,11632],{},"Firebase",[752,11634,11635,11636,11639],{},"In this article, we'll walk you through the architecture, key technical decisions, and code behind our ",[755,11637,11638],{},"Suno Remix PoC",". Whether you're a music tech enthusiast or a developer exploring AI-powered audio tools, this guide will help you build something similar.",[767,11641],{},[747,11643,11645],{"id":11644},"what-is-suno","What is Suno?",[752,11647,11648,11653],{},[2036,11649,11652],{"href":11650,"rel":11651},"https://suno.com",[2040],"Suno"," is one of the most impressive AI music generation platforms available today. It can generate full songs from text prompts, create covers in different styles, extend existing tracks, and even separate stems (vocals vs. instruments).",[752,11655,11656,11657,11660,11661,11664],{},"The catch? ",[755,11658,11659],{},"Suno doesn't have an official public API."," There's no ",[920,11662,11663],{},"suno.com/developers"," portal or API key system. Instead, a growing ecosystem of third-party services wraps Suno's capabilities behind a REST API (Application Programming Interface).",[752,11666,11667,11668,11675,11676,11679,11680,11683],{},"For this PoC, we chose ",[2036,11669,11672],{"href":11670,"rel":11671},"https://sunoapi.org",[2040],[755,11673,11674],{},"sunoapi.org"," - the most documented third-party provider with support for the ",[920,11677,11678],{},"upload-cover"," endpoint, which is what enables the \"remix\" functionality. It costs approximately ",[755,11681,11682],{},"$0.005 per credit",", which is very affordable for experimentation.",[2195,11685,11686],{},[752,11687,11688],{},"All available Suno API options are unofficial. For a proof of concept this is perfectly fine, but keep an eye on Suno's official announcements if you plan to take this to production.",[767,11690],{},[747,11692,11694],{"id":11693},"the-architecture","The Architecture",[752,11696,11697,11698,11701],{},"Here's the core challenge: Suno's API doesn't accept file uploads directly. Instead, it requires a ",[755,11699,11700],{},"public URL"," pointing to the audio file. This means we need an intermediary storage layer.",[752,11703,11704,11705,11708],{},"Our solution uses ",[755,11706,11707],{},"Firebase Storage"," as the bridge:",[4041,11710,11711,11717,11723,11732,11741],{},[782,11712,11713,11716],{},[755,11714,11715],{},"User uploads a WAV/MP3 file"," → stored in Firebase Storage (client-side SDK)",[782,11718,11719,11722],{},[755,11720,11721],{},"Firebase returns a public download URL"," → this is the key piece Suno needs",[782,11724,11725,11731],{},[755,11726,11727,11728],{},"Browser calls ",[920,11729,11730],{},"POST /api/remix"," → our Nitro server route forwards the request to sunoapi.org with the API key",[782,11733,11734,11740],{},[755,11735,11736,11737],{},"Browser polls ",[920,11738,11739],{},"GET /api/remix/:taskId"," → our server route checks sunoapi.org for the remix status every 30 seconds",[782,11742,11743,11746],{},[755,11744,11745],{},"Remix is ready"," → the browser plays both original and remixed audio side-by-side using WaveSurfer.js",[752,11748,11749],{},[755,11750,11751],{},"Why this architecture?",[1906,11753,11754,11764],{},[1909,11755,11756],{},[1912,11757,11758,11761],{},[1915,11759,11760],{},"Decision",[1915,11762,11763],{},"Reason",[1922,11765,11766,11775,11785,11795],{},[1912,11767,11768,11772],{},[1927,11769,11770],{},[755,11771,11707],{},[1927,11773,11774],{},"Gives us a publicly accessible URL for the uploaded WAV file",[1912,11776,11777,11782],{},[1927,11778,11779],{},[755,11780,11781],{},"Nitro server routes",[1927,11783,11784],{},"Keeps the Suno API key on the server - never exposed to the browser",[1912,11786,11787,11792],{},[1927,11788,11789],{},[755,11790,11791],{},"Polling",[1927,11793,11794],{},"Suno processes remixes asynchronously (1-3 min); we poll every 30 seconds",[1912,11796,11797,11802],{},[1927,11798,11799],{},[755,11800,11801],{},"WaveSurfer.js",[1927,11803,11804],{},"Industry-standard waveform visualization for comparing original vs. remixed audio",[767,11806],{},[747,11808,11810],{"id":11809},"tech-stack","Tech Stack",[1906,11812,11813,11822],{},[1909,11814,11815],{},[1912,11816,11817,11819],{},[1915,11818,2488],{},[1915,11820,11821],{},"Technology",[1922,11823,11824,11834,11844,11854,11865,11874,11884],{},[1912,11825,11826,11829],{},[1927,11827,11828],{},"Frontend",[1927,11830,11831,11833],{},[755,11832,11629],{}," + Vue 3 + Nuxt UI",[1912,11835,11836,11839],{},[1927,11837,11838],{},"Styling",[1927,11840,11841],{},[755,11842,11843],{},"Tailwind CSS 4",[1912,11845,11846,11849],{},[1927,11847,11848],{},"Audio Visualization",[1927,11850,11851],{},[755,11852,11853],{},"WaveSurfer.js 7",[1912,11855,11856,11859],{},[1927,11857,11858],{},"Server",[1927,11860,11861,11864],{},[755,11862,11863],{},"Nitro"," (built into Nuxt)",[1912,11866,11867,11870],{},[1927,11868,11869],{},"Storage",[1927,11871,11872],{},[755,11873,11707],{},[1912,11875,11876,11879],{},[1927,11877,11878],{},"Hosting",[1927,11880,11881],{},[755,11882,11883],{},"Firebase Hosting",[1912,11885,11886,11888],{},[1927,11887,4420],{},[1927,11889,11890,11892],{},[755,11891,11674],{}," (third-party Suno proxy)",[767,11894],{},[747,11896,11898],{"id":11897},"the-key-technical-challenge-upload-flow","The Key Technical Challenge: Upload Flow",[752,11900,11901],{},"The most interesting part of this project is how we bridge the gap between a user's local WAV file and Suno's requirement for a public URL.",[774,11903,11905],{"id":11904},"step-1-upload-to-firebase-storage","Step 1: Upload to Firebase Storage",[752,11907,11908,11909,11912],{},"We created a ",[920,11910,11911],{},"useFirebaseStorage"," composable that handles the upload with progress tracking:",[929,11914,11919],{"className":11915,"code":11916,"filename":11917,"language":11918,"meta":934,"style":934},"language-typescript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import { getStorage, ref as storageRef, uploadBytesResumable, getDownloadURL } from 'firebase/storage'\n\nasync function uploadWavFile(file: File): Promise\u003Cstring> {\n  const storage = getStorage(app)\n  const fileName = `${crypto.randomUUID()}.wav`\n  const fileRef = storageRef(storage, `uploads/${fileName}`)\n\n  const uploadTask = uploadBytesResumable(fileRef, file, {\n    contentType: 'audio/wav',\n  })\n\n  return new Promise((resolve, reject) => {\n    uploadTask.on('state_changed',\n      (snapshot) => {\n        // Track progress (0-100%)\n        uploadProgress.value = Math.round(\n          (snapshot.bytesTransferred / snapshot.totalBytes) * 100\n        )\n      },\n      reject,\n      async () => {\n        // Get the public download URL\n        const downloadUrl = await getDownloadURL(uploadTask.snapshot.ref)\n        resolve(downloadUrl)\n      }\n    )\n  })\n}\n","useFirebaseStorage.ts","typescript",[920,11920,11921,11964,11968,12002,12021,12052,12087,12091,12116,12132,12139,12143,12171,12192,12206,12211,12233,12263,12267,12272,12279,12291,12296,12327,12339,12344,12348,12354],{"__ignoreMap":934},[938,11922,11923,11925,11927,11930,11932,11935,11937,11940,11942,11945,11947,11950,11953,11956,11959,11962],{"class":940,"line":941},[938,11924,2618],{"class":1097},[938,11926,2777],{"class":944},[938,11928,11929],{"class":955}," getStorage",[938,11931,1085],{"class":944},[938,11933,11934],{"class":955}," ref",[938,11936,5543],{"class":1097},[938,11938,11939],{"class":955}," storageRef",[938,11941,1085],{"class":944},[938,11943,11944],{"class":955}," uploadBytesResumable",[938,11946,1085],{"class":944},[938,11948,11949],{"class":955}," getDownloadURL",[938,11951,11952],{"class":944}," }",[938,11954,11955],{"class":1097}," from",[938,11957,11958],{"class":944}," '",[938,11960,11961],{"class":1727},"firebase/storage",[938,11963,11320],{"class":944},[938,11965,11966],{"class":940,"line":967},[938,11967,1057],{"emptyLinePlaceholder":1056},[938,11969,11970,11973,11976,11979,11981,11983,11985,11988,11991,11994,11996,11998,12000],{"class":940,"line":1053},[938,11971,11972],{"class":1035},"async",[938,11974,11975],{"class":1035}," function",[938,11977,11978],{"class":1072}," uploadWavFile",[938,11980,1076],{"class":944},[938,11982,6140],{"class":2690},[938,11984,1880],{"class":944},[938,11986,11987],{"class":1020}," File",[938,11989,11990],{"class":944},"):",[938,11992,11993],{"class":1020}," Promise",[938,11995,945],{"class":944},[938,11997,972],{"class":1020},[938,11999,952],{"class":944},[938,12001,1030],{"class":944},[938,12003,12004,12007,12010,12012,12014,12016,12019],{"class":940,"line":1060},[938,12005,12006],{"class":1035},"  const",[938,12008,12009],{"class":955}," storage",[938,12011,5214],{"class":944},[938,12013,11929],{"class":1072},[938,12015,1076],{"class":948},[938,12017,12018],{"class":955},"app",[938,12020,3256],{"class":948},[938,12022,12023,12025,12028,12030,12033,12036,12038,12041,12044,12046,12049],{"class":940,"line":1066},[938,12024,12006],{"class":1035},[938,12026,12027],{"class":955}," fileName",[938,12029,5214],{"class":944},[938,12031,12032],{"class":944}," `${",[938,12034,12035],{"class":955},"crypto",[938,12037,1148],{"class":944},[938,12039,12040],{"class":1072},"randomUUID",[938,12042,12043],{"class":955},"()",[938,12045,2682],{"class":944},[938,12047,12048],{"class":1727},".wav",[938,12050,12051],{"class":944},"`\n",[938,12053,12054,12056,12059,12061,12063,12065,12068,12070,12073,12076,12079,12082,12085],{"class":940,"line":1094},[938,12055,12006],{"class":1035},[938,12057,12058],{"class":955}," fileRef",[938,12060,5214],{"class":944},[938,12062,11939],{"class":1072},[938,12064,1076],{"class":948},[938,12066,12067],{"class":955},"storage",[938,12069,1085],{"class":944},[938,12071,12072],{"class":944}," `",[938,12074,12075],{"class":1727},"uploads/",[938,12077,12078],{"class":944},"${",[938,12080,12081],{"class":955},"fileName",[938,12083,12084],{"class":944},"}`",[938,12086,3256],{"class":948},[938,12088,12089],{"class":940,"line":1116},[938,12090,1057],{"emptyLinePlaceholder":1056},[938,12092,12093,12095,12098,12100,12102,12104,12107,12109,12112,12114],{"class":940,"line":1134},[938,12094,12006],{"class":1035},[938,12096,12097],{"class":955}," uploadTask",[938,12099,5214],{"class":944},[938,12101,11944],{"class":1072},[938,12103,1076],{"class":948},[938,12105,12106],{"class":955},"fileRef",[938,12108,1085],{"class":944},[938,12110,12111],{"class":955}," file",[938,12113,1085],{"class":944},[938,12115,1030],{"class":944},[938,12117,12118,12121,12123,12125,12128,12130],{"class":940,"line":1154},[938,12119,12120],{"class":948},"    contentType",[938,12122,1880],{"class":944},[938,12124,11958],{"class":944},[938,12126,12127],{"class":1727},"audio/wav",[938,12129,10753],{"class":944},[938,12131,2716],{"class":944},[938,12133,12134,12137],{"class":940,"line":1171},[938,12135,12136],{"class":944},"  }",[938,12138,3256],{"class":948},[938,12140,12141],{"class":940,"line":1191},[938,12142,1057],{"emptyLinePlaceholder":1056},[938,12144,12145,12147,12150,12152,12154,12156,12159,12161,12164,12166,12169],{"class":940,"line":1196},[938,12146,1509],{"class":1097},[938,12148,12149],{"class":944}," new",[938,12151,11993],{"class":1020},[938,12153,1076],{"class":948},[938,12155,1076],{"class":944},[938,12157,12158],{"class":2690},"resolve",[938,12160,1085],{"class":944},[938,12162,12163],{"class":2690}," reject",[938,12165,1731],{"class":944},[938,12167,12168],{"class":1035}," =>",[938,12170,1030],{"class":944},[938,12172,12173,12176,12178,12181,12183,12185,12188,12190],{"class":940,"line":1231},[938,12174,12175],{"class":955},"    uploadTask",[938,12177,1148],{"class":944},[938,12179,12180],{"class":1072},"on",[938,12182,1076],{"class":948},[938,12184,10753],{"class":944},[938,12186,12187],{"class":1727},"state_changed",[938,12189,10753],{"class":944},[938,12191,2716],{"class":944},[938,12193,12194,12197,12200,12202,12204],{"class":940,"line":1237},[938,12195,12196],{"class":944},"      (",[938,12198,12199],{"class":2690},"snapshot",[938,12201,1731],{"class":944},[938,12203,12168],{"class":1035},[938,12205,1030],{"class":944},[938,12207,12208],{"class":940,"line":1243},[938,12209,12210],{"class":1535},"        // Track progress (0-100%)\n",[938,12212,12213,12216,12218,12221,12223,12226,12228,12231],{"class":940,"line":2431},[938,12214,12215],{"class":955},"        uploadProgress",[938,12217,1148],{"class":944},[938,12219,12220],{"class":955},"value",[938,12222,5214],{"class":944},[938,12224,12225],{"class":955}," Math",[938,12227,1148],{"class":944},[938,12229,12230],{"class":1072},"round",[938,12232,3210],{"class":948},[938,12234,12235,12238,12240,12242,12245,12247,12250,12252,12255,12258,12260],{"class":940,"line":2437},[938,12236,12237],{"class":948},"          (",[938,12239,12199],{"class":955},[938,12241,1148],{"class":944},[938,12243,12244],{"class":955},"bytesTransferred",[938,12246,1526],{"class":944},[938,12248,12249],{"class":955}," snapshot",[938,12251,1148],{"class":944},[938,12253,12254],{"class":955},"totalBytes",[938,12256,12257],{"class":948},") ",[938,12259,1358],{"class":944},[938,12261,12262],{"class":1522}," 100\n",[938,12264,12265],{"class":940,"line":2443},[938,12266,6170],{"class":948},[938,12268,12269],{"class":940,"line":2449},[938,12270,12271],{"class":944},"      },\n",[938,12273,12274,12277],{"class":940,"line":2454},[938,12275,12276],{"class":955},"      reject",[938,12278,2716],{"class":944},[938,12280,12281,12284,12287,12289],{"class":940,"line":2460},[938,12282,12283],{"class":1035},"      async",[938,12285,12286],{"class":944}," ()",[938,12288,12168],{"class":1035},[938,12290,1030],{"class":944},[938,12292,12293],{"class":940,"line":2466},[938,12294,12295],{"class":1535},"        // Get the public download URL\n",[938,12297,12298,12301,12304,12306,12309,12311,12313,12316,12318,12320,12322,12325],{"class":940,"line":2472},[938,12299,12300],{"class":1035},"        const",[938,12302,12303],{"class":955}," downloadUrl",[938,12305,5214],{"class":944},[938,12307,12308],{"class":1097}," await",[938,12310,11949],{"class":1072},[938,12312,1076],{"class":948},[938,12314,12315],{"class":955},"uploadTask",[938,12317,1148],{"class":944},[938,12319,12199],{"class":955},[938,12321,1148],{"class":944},[938,12323,12324],{"class":955},"ref",[938,12326,3256],{"class":948},[938,12328,12329,12332,12334,12337],{"class":940,"line":3049},[938,12330,12331],{"class":1072},"        resolve",[938,12333,1076],{"class":948},[938,12335,12336],{"class":955},"downloadUrl",[938,12338,3256],{"class":948},[938,12340,12341],{"class":940,"line":3071},[938,12342,12343],{"class":944},"      }\n",[938,12345,12346],{"class":940,"line":3149},[938,12347,3359],{"class":948},[938,12349,12350,12352],{"class":940,"line":3154},[938,12351,12136],{"class":944},[938,12353,3256],{"class":948},[938,12355,12356],{"class":940,"line":3159},[938,12357,1246],{"class":944},[752,12359,3806,12360,12363],{},[920,12361,12362],{},"getDownloadURL()"," call returns a publicly accessible Firebase Storage URL - exactly what Suno needs.",[774,12365,12367],{"id":12366},"step-2-call-the-suno-api-via-server-proxy","Step 2: Call the Suno API (via server proxy)",[752,12369,12370],{},"We never call Suno directly from the browser. Instead, we use a Nitro server route that reads the API key from server-only runtime configuration:",[929,12372,12375],{"className":11915,"code":12373,"filename":12374,"language":11918,"meta":934,"style":934},"export default defineEventHandler(async (event) => {\n  const body = await readBody(event)\n  const config = useRuntimeConfig()\n\n  const result = await fetch('https://api.sunoapi.org/api/v1/generate/upload-cover', {\n    method: 'POST',\n    headers: {\n      'Authorization': `Bearer ${config.sunoApiKey}`,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify({\n      uploadUrl: body.uploadUrl,\n      customMode: true,\n      style: body.style,\n      title: body.title,\n      model: 'V4_5',\n    }),\n  })\n\n  const data = await result.json()\n  return { taskId: data.data.taskId }\n})\n","server/api/remix.post.ts",[920,12376,12377,12401,12421,12435,12439,12465,12480,12488,12518,12536,12541,12561,12577,12590,12605,12620,12636,12645,12651,12655,12673,12698],{"__ignoreMap":934},[938,12378,12379,12382,12384,12387,12389,12391,12393,12395,12397,12399],{"class":940,"line":941},[938,12380,12381],{"class":1097},"export",[938,12383,4784],{"class":1097},[938,12385,12386],{"class":1072}," defineEventHandler",[938,12388,1076],{"class":955},[938,12390,11972],{"class":1035},[938,12392,1101],{"class":944},[938,12394,7231],{"class":2690},[938,12396,1731],{"class":944},[938,12398,12168],{"class":1035},[938,12400,1030],{"class":944},[938,12402,12403,12405,12408,12410,12412,12415,12417,12419],{"class":940,"line":967},[938,12404,12006],{"class":1035},[938,12406,12407],{"class":955}," body",[938,12409,5214],{"class":944},[938,12411,12308],{"class":1097},[938,12413,12414],{"class":1072}," readBody",[938,12416,1076],{"class":948},[938,12418,7231],{"class":955},[938,12420,3256],{"class":948},[938,12422,12423,12425,12428,12430,12433],{"class":940,"line":1053},[938,12424,12006],{"class":1035},[938,12426,12427],{"class":955}," config",[938,12429,5214],{"class":944},[938,12431,12432],{"class":1072}," useRuntimeConfig",[938,12434,1131],{"class":948},[938,12436,12437],{"class":940,"line":1060},[938,12438,1057],{"emptyLinePlaceholder":1056},[938,12440,12441,12443,12445,12447,12449,12452,12454,12456,12459,12461,12463],{"class":940,"line":1066},[938,12442,12006],{"class":1035},[938,12444,8805],{"class":955},[938,12446,5214],{"class":944},[938,12448,12308],{"class":1097},[938,12450,12451],{"class":1072}," fetch",[938,12453,1076],{"class":948},[938,12455,10753],{"class":944},[938,12457,12458],{"class":1727},"https://api.sunoapi.org/api/v1/generate/upload-cover",[938,12460,10753],{"class":944},[938,12462,1085],{"class":944},[938,12464,1030],{"class":944},[938,12466,12467,12470,12472,12474,12476,12478],{"class":940,"line":1094},[938,12468,12469],{"class":948},"    method",[938,12471,1880],{"class":944},[938,12473,11958],{"class":944},[938,12475,11462],{"class":1727},[938,12477,10753],{"class":944},[938,12479,2716],{"class":944},[938,12481,12482,12484,12486],{"class":940,"line":1116},[938,12483,3245],{"class":948},[938,12485,1880],{"class":944},[938,12487,1030],{"class":944},[938,12489,12490,12493,12495,12497,12499,12501,12504,12506,12509,12511,12514,12516],{"class":940,"line":1134},[938,12491,12492],{"class":944},"      '",[938,12494,2782],{"class":948},[938,12496,10753],{"class":944},[938,12498,1880],{"class":944},[938,12500,12072],{"class":944},[938,12502,12503],{"class":1727},"Bearer ",[938,12505,12078],{"class":944},[938,12507,12508],{"class":955},"config",[938,12510,1148],{"class":944},[938,12512,12513],{"class":955},"sunoApiKey",[938,12515,12084],{"class":944},[938,12517,2716],{"class":944},[938,12519,12520,12522,12524,12526,12528,12530,12532,12534],{"class":940,"line":1154},[938,12521,12492],{"class":944},[938,12523,5727],{"class":948},[938,12525,10753],{"class":944},[938,12527,1880],{"class":944},[938,12529,11958],{"class":944},[938,12531,6422],{"class":1727},[938,12533,10753],{"class":944},[938,12535,2716],{"class":944},[938,12537,12538],{"class":940,"line":1171},[938,12539,12540],{"class":944},"    },\n",[938,12542,12543,12546,12548,12551,12553,12556,12558],{"class":940,"line":1191},[938,12544,12545],{"class":948},"    body",[938,12547,1880],{"class":944},[938,12549,12550],{"class":955}," JSON",[938,12552,1148],{"class":944},[938,12554,12555],{"class":1072},"stringify",[938,12557,1076],{"class":948},[938,12559,12560],{"class":944},"{\n",[938,12562,12563,12566,12568,12570,12572,12575],{"class":940,"line":1196},[938,12564,12565],{"class":948},"      uploadUrl",[938,12567,1880],{"class":944},[938,12569,12407],{"class":955},[938,12571,1148],{"class":944},[938,12573,12574],{"class":955},"uploadUrl",[938,12576,2716],{"class":944},[938,12578,12579,12582,12584,12588],{"class":940,"line":1231},[938,12580,12581],{"class":948},"      customMode",[938,12583,1880],{"class":944},[938,12585,12587],{"class":12586},"sfNiH"," true",[938,12589,2716],{"class":944},[938,12591,12592,12595,12597,12599,12601,12603],{"class":940,"line":1237},[938,12593,12594],{"class":948},"      style",[938,12596,1880],{"class":944},[938,12598,12407],{"class":955},[938,12600,1148],{"class":944},[938,12602,2043],{"class":955},[938,12604,2716],{"class":944},[938,12606,12607,12610,12612,12614,12616,12618],{"class":940,"line":1243},[938,12608,12609],{"class":948},"      title",[938,12611,1880],{"class":944},[938,12613,12407],{"class":955},[938,12615,1148],{"class":944},[938,12617,2862],{"class":955},[938,12619,2716],{"class":944},[938,12621,12622,12625,12627,12629,12632,12634],{"class":940,"line":2431},[938,12623,12624],{"class":948},"      model",[938,12626,1880],{"class":944},[938,12628,11958],{"class":944},[938,12630,12631],{"class":1727},"V4_5",[938,12633,10753],{"class":944},[938,12635,2716],{"class":944},[938,12637,12638,12641,12643],{"class":940,"line":2437},[938,12639,12640],{"class":944},"    }",[938,12642,1731],{"class":948},[938,12644,2716],{"class":944},[938,12646,12647,12649],{"class":940,"line":2443},[938,12648,12136],{"class":944},[938,12650,3256],{"class":948},[938,12652,12653],{"class":940,"line":2449},[938,12654,1057],{"emptyLinePlaceholder":1056},[938,12656,12657,12659,12661,12663,12665,12667,12669,12671],{"class":940,"line":2454},[938,12658,12006],{"class":1035},[938,12660,5797],{"class":955},[938,12662,5214],{"class":944},[938,12664,12308],{"class":1097},[938,12666,8805],{"class":955},[938,12668,1148],{"class":944},[938,12670,2754],{"class":1072},[938,12672,1131],{"class":948},[938,12674,12675,12677,12679,12682,12684,12686,12688,12690,12692,12695],{"class":940,"line":2460},[938,12676,1509],{"class":1097},[938,12678,2777],{"class":944},[938,12680,12681],{"class":948}," taskId",[938,12683,1880],{"class":944},[938,12685,5797],{"class":955},[938,12687,1148],{"class":944},[938,12689,10019],{"class":955},[938,12691,1148],{"class":944},[938,12693,12694],{"class":955},"taskId",[938,12696,12697],{"class":944}," }\n",[938,12699,12700,12702],{"class":940,"line":2466},[938,12701,2682],{"class":944},[938,12703,3256],{"class":955},[2330,12705,12706],{},[752,12707,3806,12708,12711,12712,12715,12716,12719,12720,12723],{},[920,12709,12710],{},"NUXT_SUNO_API_KEY"," environment variable is mapped to ",[920,12713,12714],{},"runtimeConfig.sunoApiKey"," (without ",[920,12717,12718],{},"public","), which means Nuxt ",[755,12721,12722],{},"never bundles it into client-side JavaScript",". This is the correct way to handle API secrets in Nuxt.",[774,12725,12727],{"id":12726},"step-3-poll-for-results","Step 3: Poll for Results",[752,12729,12730],{},"Suno processes remixes asynchronously. We poll every 30 seconds:",[929,12732,12735],{"className":11915,"code":12733,"filename":12734,"language":11918,"meta":934,"style":934},"pollInterval = setInterval(async () => {\n  const result = await $fetch(`/api/remix/${taskId.value}`)\n\n  if (result.status === 'SUCCESS') {\n    tracks.value = result.tracks\n    status.value = 'completed'\n    stopPolling()\n  }\n}, 30000)\n","useSunoRemix.ts",[920,12736,12737,12757,12790,12794,12822,12840,12858,12865,12869],{"__ignoreMap":934},[938,12738,12739,12742,12744,12747,12749,12751,12753,12755],{"class":940,"line":941},[938,12740,12741],{"class":955},"pollInterval ",[938,12743,1125],{"class":944},[938,12745,12746],{"class":1072}," setInterval",[938,12748,1076],{"class":955},[938,12750,11972],{"class":1035},[938,12752,12286],{"class":944},[938,12754,12168],{"class":1035},[938,12756,1030],{"class":944},[938,12758,12759,12761,12763,12765,12767,12770,12772,12775,12778,12780,12782,12784,12786,12788],{"class":940,"line":967},[938,12760,12006],{"class":1035},[938,12762,8805],{"class":955},[938,12764,5214],{"class":944},[938,12766,12308],{"class":1097},[938,12768,12769],{"class":1072}," $fetch",[938,12771,1076],{"class":948},[938,12773,12774],{"class":944},"`",[938,12776,12777],{"class":1727},"/api/remix/",[938,12779,12078],{"class":944},[938,12781,12694],{"class":955},[938,12783,1148],{"class":944},[938,12785,12220],{"class":955},[938,12787,12084],{"class":944},[938,12789,3256],{"class":948},[938,12791,12792],{"class":940,"line":1053},[938,12793,1057],{"emptyLinePlaceholder":1056},[938,12795,12796,12799,12801,12804,12806,12808,12811,12813,12816,12818,12820],{"class":940,"line":1060},[938,12797,12798],{"class":1097},"  if",[938,12800,1101],{"class":948},[938,12802,12803],{"class":955},"result",[938,12805,1148],{"class":944},[938,12807,8862],{"class":955},[938,12809,12810],{"class":944}," ===",[938,12812,11958],{"class":944},[938,12814,12815],{"class":1727},"SUCCESS",[938,12817,10753],{"class":944},[938,12819,12257],{"class":948},[938,12821,12560],{"class":944},[938,12823,12824,12827,12829,12831,12833,12835,12837],{"class":940,"line":1066},[938,12825,12826],{"class":955},"    tracks",[938,12828,1148],{"class":944},[938,12830,12220],{"class":955},[938,12832,5214],{"class":944},[938,12834,8805],{"class":955},[938,12836,1148],{"class":944},[938,12838,12839],{"class":955},"tracks\n",[938,12841,12842,12845,12847,12849,12851,12853,12856],{"class":940,"line":1094},[938,12843,12844],{"class":955},"    status",[938,12846,1148],{"class":944},[938,12848,12220],{"class":955},[938,12850,5214],{"class":944},[938,12852,11958],{"class":944},[938,12854,12855],{"class":1727},"completed",[938,12857,11320],{"class":944},[938,12859,12860,12863],{"class":940,"line":1116},[938,12861,12862],{"class":1072},"    stopPolling",[938,12864,1131],{"class":948},[938,12866,12867],{"class":940,"line":1134},[938,12868,1240],{"class":944},[938,12870,12871,12873,12876],{"class":940,"line":1154},[938,12872,9986],{"class":944},[938,12874,12875],{"class":1522}," 30000",[938,12877,3256],{"class":955},[752,12879,12880,12881,12884],{},"When the remix is ready, the response includes an ",[920,12882,12883],{},"audio_url"," pointing to the generated track on Suno's CDN (Content Delivery Network).",[767,12886],{},[747,12888,12890],{"id":12889},"building-the-frontend","Building the Frontend",[752,12892,12893],{},"The UI is intentionally simple - this is a PoC, after all. The entire flow happens on a single page:",[4041,12895,12896,12902,12908,12914],{},[782,12897,12898,12901],{},[755,12899,12900],{},"UploadZone"," - Drag-and-drop or file picker with WAV/MP3 validation (max 50 MB)",[782,12903,12904,12907],{},[755,12905,12906],{},"StyleSelector"," - Genre dropdown (16 genres), title input, and instrumental toggle",[782,12909,12910,12913],{},[755,12911,12912],{},"ProgressIndicator"," - 3-step progress bar with upload percentage",[782,12915,12916,12919],{},[755,12917,12918],{},"AudioComparison"," - Side-by-side WaveSurfer.js players for original and remixed audio",[752,12921,12922],{},"Here's what the upload screen looks like:",[752,12924,12925],{},[12926,12927],"img",{"alt":12928,"src":12929},"Upload & Configure - drag-drop zone with genre selector and Remix button","/images/blog/suno-remix-poc-upload.webp",[752,12931,12932,12933,12936],{},"Once you hit ",[755,12934,12935],{},"Remix",", the app uploads the file to Firebase Storage and kicks off the AI processing. The 3-step progress indicator keeps you informed:",[752,12938,12939],{},[12926,12940],{"alt":12941,"src":12942},"Progress indicator - Upload to Cloud → AI Remix → Done","/images/blog/suno-remix-poc-progress.webp",[752,12944,12945],{},"When the remix is complete, you get a side-by-side comparison with waveform visualization - the original on the left, and the AI-generated remix(es) alongside it:",[752,12947,12948],{},[12926,12949],{"alt":12950,"src":12951},"Results - original and remixed audio with WaveSurfer.js waveforms","/images/blog/suno-remix-poc-results.webp",[774,12953,12955],{"id":12954},"wavesurferjs-integration","WaveSurfer.js Integration",[752,12957,12958,12959,12962],{},"We wrapped WaveSurfer.js in a Vue component with ",[920,12960,12961],{},"\u003CClientOnly>"," to avoid SSR (Server-Side Rendering) issues:",[929,12964,12969],{"className":12965,"code":12966,"filename":12967,"language":12968,"meta":934,"style":934},"language-vue shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003CClientOnly>\n  \u003CWaveformPlayer\n    :audio-url=\"track.audioUrl\"\n    label=\"Remix - Jazz\"\n  />\n\u003C/ClientOnly>\n","WaveformPlayer.vue","vue",[920,12970,12971,12980,12985,12990,12995,13000],{"__ignoreMap":934},[938,12972,12973,12975,12978],{"class":940,"line":941},[938,12974,945],{"class":944},[938,12976,12977],{"class":948},"ClientOnly",[938,12979,964],{"class":944},[938,12981,12982],{"class":940,"line":967},[938,12983,12984],{"class":955},"  \u003CWaveformPlayer\n",[938,12986,12987],{"class":940,"line":1053},[938,12988,12989],{"class":955},"    :audio-url=\"track.audioUrl\"\n",[938,12991,12992],{"class":940,"line":1060},[938,12993,12994],{"class":955},"    label=\"Remix - Jazz\"\n",[938,12996,12997],{"class":940,"line":1066},[938,12998,12999],{"class":955},"  />\n",[938,13001,13002,13004,13006],{"class":940,"line":1094},[938,13003,959],{"class":944},[938,13005,12977],{"class":948},[938,13007,964],{"class":944},[752,13009,13010],{},"The player provides play/pause controls, a progress bar, and time display - all styled to match the dark theme.",[767,13012],{},[747,13014,13016],{"id":13015},"deployment-to-firebase","Deployment to Firebase",[752,13018,13019],{},"Firebase Hosting serves the Nuxt static output. The setup is straightforward:",[929,13021,13025],{"className":13022,"code":13023,"filename":13024,"language":2754,"meta":934,"style":934},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{\n  \"hosting\": {\n    \"public\": \".output/public\",\n    \"cleanUrls\": true,\n    \"rewrites\": [\n      { \"source\": \"**\", \"destination\": \"/index.html\" }\n    ]\n  },\n  \"storage\": {\n    \"rules\": \"firebase-storage.rules\"\n  }\n}\n","firebase.json",[920,13026,13027,13031,13045,13064,13078,13091,13132,13137,13142,13154,13172,13176],{"__ignoreMap":934},[938,13028,13029],{"class":940,"line":941},[938,13030,12560],{"class":944},[938,13032,13033,13036,13039,13041,13043],{"class":940,"line":967},[938,13034,13035],{"class":944},"  \"",[938,13037,13038],{"class":1035},"hosting",[938,13040,2673],{"class":944},[938,13042,1880],{"class":944},[938,13044,1030],{"class":944},[938,13046,13047,13049,13051,13053,13055,13057,13060,13062],{"class":940,"line":1053},[938,13048,2699],{"class":944},[938,13050,12718],{"class":1020},[938,13052,2673],{"class":944},[938,13054,1880],{"class":944},[938,13056,2635],{"class":944},[938,13058,13059],{"class":1727},".output/public",[938,13061,2673],{"class":944},[938,13063,2716],{"class":944},[938,13065,13066,13068,13071,13073,13075],{"class":940,"line":1060},[938,13067,2699],{"class":944},[938,13069,13070],{"class":1020},"cleanUrls",[938,13072,2673],{"class":944},[938,13074,1880],{"class":944},[938,13076,13077],{"class":944}," true,\n",[938,13079,13080,13082,13085,13087,13089],{"class":940,"line":1066},[938,13081,2699],{"class":944},[938,13083,13084],{"class":1020},"rewrites",[938,13086,2673],{"class":944},[938,13088,1880],{"class":944},[938,13090,2909],{"class":944},[938,13092,13093,13096,13098,13101,13103,13105,13107,13110,13112,13114,13116,13119,13121,13123,13125,13128,13130],{"class":940,"line":1094},[938,13094,13095],{"class":944},"      {",[938,13097,2635],{"class":944},[938,13099,13100],{"class":1522},"source",[938,13102,2673],{"class":944},[938,13104,1880],{"class":944},[938,13106,2635],{"class":944},[938,13108,13109],{"class":1727},"**",[938,13111,2673],{"class":944},[938,13113,1085],{"class":944},[938,13115,2635],{"class":944},[938,13117,13118],{"class":1522},"destination",[938,13120,2673],{"class":944},[938,13122,1880],{"class":944},[938,13124,2635],{"class":944},[938,13126,13127],{"class":1727},"/index.html",[938,13129,2673],{"class":944},[938,13131,12697],{"class":944},[938,13133,13134],{"class":940,"line":1116},[938,13135,13136],{"class":944},"    ]\n",[938,13138,13139],{"class":940,"line":1134},[938,13140,13141],{"class":944},"  },\n",[938,13143,13144,13146,13148,13150,13152],{"class":940,"line":1154},[938,13145,13035],{"class":944},[938,13147,12067],{"class":1035},[938,13149,2673],{"class":944},[938,13151,1880],{"class":944},[938,13153,1030],{"class":944},[938,13155,13156,13158,13161,13163,13165,13167,13170],{"class":940,"line":1171},[938,13157,2699],{"class":944},[938,13159,13160],{"class":1020},"rules",[938,13162,2673],{"class":944},[938,13164,1880],{"class":944},[938,13166,2635],{"class":944},[938,13168,13169],{"class":1727},"firebase-storage.rules",[938,13171,2641],{"class":944},[938,13173,13174],{"class":940,"line":1191},[938,13175,1240],{"class":944},[938,13177,13178],{"class":940,"line":1196},[938,13179,1246],{"class":944},[752,13181,13182],{},"Firebase Storage rules restrict uploads to WAV and MP3 files under 50 MB:",[929,13184,13187],{"className":13185,"code":13186,"language":8289},[8287],"match /uploads/{fileName} {\n  allow write: if request.resource.size \u003C 50 * 1024 * 1024\n               && (request.resource.contentType == 'audio/wav'\n                   || request.resource.contentType == 'audio/mpeg');\n  allow read: if true;\n}\n",[920,13188,13186],{"__ignoreMap":934},[752,13190,13191],{},"Deploy with:",[929,13193,13195],{"className":4657,"code":13194,"language":4659,"meta":934,"style":934},"pnpm build\nfirebase deploy\n",[920,13196,13197,13205],{"__ignoreMap":934},[938,13198,13199,13202],{"class":940,"line":941},[938,13200,13201],{"class":1020},"pnpm",[938,13203,13204],{"class":1727}," build\n",[938,13206,13207,13210],{"class":940,"line":967},[938,13208,13209],{"class":1020},"firebase",[938,13211,13212],{"class":1727}," deploy\n",[767,13214],{},[747,13216,13218],{"id":13217},"security-considerations","Security Considerations",[752,13220,13221],{},"Even for a PoC, security matters:",[1906,13223,13224,13234],{},[1909,13225,13226],{},[1912,13227,13228,13231],{},[1915,13229,13230],{},"Concern",[1915,13232,13233],{},"Solution",[1922,13235,13236,13253,13263,13273],{},[1912,13237,13238,13243],{},[1927,13239,13240],{},[755,13241,13242],{},"API key exposure",[1927,13244,13245,13246,13249,13250],{},"Stored in ",[920,13247,13248],{},"runtimeConfig"," (server-only), never in ",[920,13251,13252],{},"runtimeConfig.public",[1912,13254,13255,13260],{},[1927,13256,13257],{},[755,13258,13259],{},"File abuse",[1927,13261,13262],{},"Firebase Storage rules: WAV/MP3 only, 50 MB max",[1912,13264,13265,13270],{},[1927,13266,13267],{},[755,13268,13269],{},"Input validation",[1927,13271,13272],{},"Server routes validate URL format, string lengths",[1912,13274,13275,13280],{},[1927,13276,13277],{},[755,13278,13279],{},"Secrets in git",[1927,13281,13282,13284,13285,13288,13289,13292],{},[920,13283,4653],{}," in ",[920,13286,13287],{},".gitignore","; only ",[920,13290,13291],{},".env.example"," committed",[767,13294],{},[747,13296,13298],{"id":13297},"watch-out-copyright-detection","Watch Out: Copyright Detection",[752,13300,13301,13302,13305],{},"One thing that caught us off guard during testing - Suno has ",[755,13303,13304],{},"built-in copyright detection",". When we tried remixing a well-known track (AC/DC's \"Back in Black\"), the API returned an error:",[929,13307,13310],{"className":13308,"code":13309,"language":8289},[8287],"Error code: 413\nError message: Uploaded audio matches existing work of art.\n",[920,13311,13309],{"__ignoreMap":934},[752,13313,13314,13315,13318],{},"Suno's system fingerprints uploaded audio and compares it against known copyrighted works. If it detects a match, the remix is rejected with a ",[920,13316,13317],{},"GENERATE_AUDIO_FAILED"," status.",[2204,13320,13321],{},[752,13322,13323,13324,13327],{},"Only upload ",[755,13325,13326],{},"original recordings"," or content you have rights to. Suno will reject copyrighted material, and repeated violations may affect your API account.",[752,13329,13330],{},"This is actually a responsible feature - it prevents the platform from being used to create unauthorized covers or derivatives of copyrighted music. For our PoC, we switched to an original recording and everything worked perfectly.",[767,13332],{},[747,13334,13336],{"id":13335},"what-we-learned","What We Learned",[4041,13338,13339,13345,13351,13357,13363],{},[782,13340,13341,13344],{},[755,13342,13343],{},"Suno's \"upload-cover\" endpoint is powerful"," - It takes an audio file and re-imagines it in a completely different style while retaining the core melody. The results can be surprisingly good.",[782,13346,13347,13350],{},[755,13348,13349],{},"Copyright detection is real"," - Don't assume you can remix any audio. Suno actively fingerprints uploads and blocks copyrighted material with error code 413.",[782,13352,13353,13356],{},[755,13354,13355],{},"The public URL requirement adds complexity"," - Having to upload to Firebase Storage first adds a step, but it's a clean pattern that keeps the architecture simple.",[782,13358,13359,13362],{},[755,13360,13361],{},"Polling is fine for a PoC"," - While webhooks would be more efficient, the 30-second polling interval works well for a demo. The typical remix takes 1-3 minutes.",[782,13364,13365,13368],{},[755,13366,13367],{},"Nuxt 4 + Nitro is a great combo"," - Server routes give you a built-in API proxy without needing a separate backend service. Perfect for PoCs and small apps.",[767,13370],{},[747,13372,13374],{"id":13373},"next-steps","Next Steps",[752,13376,13377],{},"This PoC demonstrates the core flow, but there are plenty of ways to extend it:",[779,13379,13380,13386,13392,13398,13404],{},[782,13381,13382,13385],{},[755,13383,13384],{},"Multiple styles per remix"," - Let users generate several style variations at once",[782,13387,13388,13391],{},[755,13389,13390],{},"Audio trimming"," - Add a waveform-based trimmer before sending to Suno",[782,13393,13394,13397],{},[755,13395,13396],{},"User accounts"," - Save remix history with Firebase Auth",[782,13399,13400,13403],{},[755,13401,13402],{},"Batch processing"," - Upload multiple files and queue remixes",[782,13405,13406,13409],{},[755,13407,13408],{},"Stem separation"," - Use Suno's stems endpoint to separate vocals and instruments",[767,13411],{},[747,13413,13415],{"id":13414},"try-it-yourself","Try It Yourself",[779,13417,13418],{},[782,13419,13420,13423,13424],{},[755,13421,13422],{},"Live demo",": ",[2036,13425,13428],{"href":13426,"rel":13427},"https://mtl-suno-poc.web.app",[2040],"mtl-suno-poc.web.app",[752,13430,13431,13432,13435],{},"You'll need your own Suno API key from ",[2036,13433,11674],{"href":11670,"rel":13434},[2040]," and a Firebase project if you want to build something similar.",[767,13437],{},[747,13439,13441],{"id":13440},"related-articles","Related Articles",[779,13443,13444,13449,13454],{},[782,13445,13446],{},[2036,13447,13448],{"href":153},"How to Use Epidemic Sound MCP with Claude",[782,13450,13451],{},[2036,13452,13453],{"href":101},"Automatic Song Structure Analysis: How AI Detects Intro, Verse, Chorus",[782,13455,13456],{},[2036,13457,13458],{"href":475},"Exporting Ableton Live Locators to JSON with Max for Live",[2043,13460,13461],{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":934,"searchDepth":967,"depth":967,"links":13463},[13464,13465,13466,13467,13472,13475,13476,13477,13478,13479,13480,13481],{"id":11644,"depth":967,"text":11645},{"id":11693,"depth":967,"text":11694},{"id":11809,"depth":967,"text":11810},{"id":11897,"depth":967,"text":11898,"children":13468},[13469,13470,13471],{"id":11904,"depth":1053,"text":11905},{"id":12366,"depth":1053,"text":12367},{"id":12726,"depth":1053,"text":12727},{"id":12889,"depth":967,"text":12890,"children":13473},[13474],{"id":12954,"depth":1053,"text":12955},{"id":13015,"depth":967,"text":13016},{"id":13217,"depth":967,"text":13218},{"id":13297,"depth":967,"text":13298},{"id":13335,"depth":967,"text":13336},{"id":13373,"depth":967,"text":13374},{"id":13414,"depth":967,"text":13415},{"id":13440,"depth":967,"text":13441},"2026-02-06T00:00:00.000Z","A step-by-step guide to building a web app that remixes audio using Suno's AI, Nuxt 4, and Firebase Storage.",{"src":13485},"/images/blog/musictechlab_blog_suno-remix-poc.webp",{"enabled":1056,"items":13487},[13488,13490,13493,13496],{"text":13489,"icon":4407},"Suno has no official public API; third-party wrappers like sunoapi.org cost about $0.005 per credit.",{"text":13491,"icon":13492},"Firebase Storage bridges the gap between local file uploads and Suno's public URL requirement.","i-lucide-cloud",{"text":13494,"icon":13495},"Suno's built-in copyright detection rejects known copyrighted material with error code 413.","i-lucide-shield",{"text":13497,"icon":13498},"Nitro server routes keep the API key server-side so it never reaches the browser.","i-lucide-lock",{},{"title":116,"description":13483},[13502,2094,13503],"AI","audio-analysis","vdlUk3pZwxfUadk0bx3d02X4tJOxLv2oe4g_c_OXEQE",{"id":13506,"title":148,"authors":13507,"badge":742,"body":13511,"category":4398,"client":742,"date":15650,"description":15651,"extension":2074,"faq":742,"featured":69,"featuredOrder":742,"hidden":69,"image":15652,"keyTakeaways":15654,"meta":15666,"navigation":1056,"path":149,"seo":15667,"status":742,"stem":150,"tags":15668,"teaser":742,"__hash__":15669,"score":941},"posts/blog/music-data/how-to-transcribe-video-to-text-using-whisper.md",[13508],{"name":738,"to":13509,"avatar":13510},"https://www.linkedin.com/in/mariusz-smenzyk",{"src":741},{"type":744,"value":13512,"toc":15618},[13513,13515,13522,13525,13527,13531,13542,13544,13548,13551,13555,13571,13575,13601,13605,13614,13617,13630,13632,13636,13639,13654,13657,13686,13688,13692,13695,13728,13731,13757,13760,13794,13796,13800,13804,13829,13833,13938,13946,13950,13976,13993,13995,13999,14002,14006,14033,14037,14061,14065,14072,14078,14080,14084,14087,14876,14879,14891,14893,14897,14900,15112,15115,15121,15123,15127,15130,15266,15268,15272,15276,15290,15350,15354,15357,15391,15395,15410,15471,15473,15477,15546,15549,15560,15562,15564,15567,15585,15588,15590,15592,15615],[747,13514,750],{"id":749},[752,13516,13517,13518,13521],{},"Got a recorded interview, meeting, or podcast that needs transcription? Instead of paying for online transcription services, you can use ",[755,13519,13520],{},"OpenAI Whisper"," - a free, open-source speech recognition model that runs locally on your machine.",[752,13523,13524],{},"In this guide, we'll walk through the complete pipeline: from video file to ready-to-use text transcription.",[767,13526],{},[747,13528,13530],{"id":13529},"requirements","Requirements",[779,13532,13533,13536,13539],{},[782,13534,13535],{},"Python 3.8+",[782,13537,13538],{},"FFmpeg (for audio/video conversion)",[782,13540,13541],{},"2-10 GB of free disk space (depending on model size)",[767,13543],{},[747,13545,13547],{"id":13546},"step-1-install-ffmpeg","Step 1: Install FFmpeg",[752,13549,13550],{},"FFmpeg is needed to extract audio from video files.",[774,13552,13554],{"id":13553},"macos-homebrew","macOS (Homebrew)",[929,13556,13558],{"className":4657,"code":13557,"language":4659,"meta":934,"style":934},"brew install ffmpeg\n",[920,13559,13560],{"__ignoreMap":934},[938,13561,13562,13565,13568],{"class":940,"line":941},[938,13563,13564],{"class":1020},"brew",[938,13566,13567],{"class":1727}," install",[938,13569,13570],{"class":1727}," ffmpeg\n",[774,13572,13574],{"id":13573},"ubuntudebian","Ubuntu/Debian",[929,13576,13578],{"className":4657,"code":13577,"language":4659,"meta":934,"style":934},"sudo apt update\nsudo apt install ffmpeg\n",[920,13579,13580,13591],{"__ignoreMap":934},[938,13581,13582,13585,13588],{"class":940,"line":941},[938,13583,13584],{"class":1020},"sudo",[938,13586,13587],{"class":1727}," apt",[938,13589,13590],{"class":1727}," update\n",[938,13592,13593,13595,13597,13599],{"class":940,"line":967},[938,13594,13584],{"class":1020},[938,13596,13587],{"class":1727},[938,13598,13567],{"class":1727},[938,13600,13570],{"class":1727},[774,13602,13604],{"id":13603},"windows","Windows",[752,13606,13607,13608,13613],{},"Download from ",[2036,13609,13612],{"href":13610,"rel":13611},"https://ffmpeg.org/download.html",[2040],"ffmpeg.org"," and add to PATH.",[752,13615,13616],{},"Verify installation:",[929,13618,13620],{"className":4657,"code":13619,"language":4659,"meta":934,"style":934},"ffmpeg -version\n",[920,13621,13622],{"__ignoreMap":934},[938,13623,13624,13627],{"class":940,"line":941},[938,13625,13626],{"class":1020},"ffmpeg",[938,13628,13629],{"class":1727}," -version\n",[767,13631],{},[747,13633,13635],{"id":13634},"step-2-install-openai-whisper","Step 2: Install OpenAI Whisper",[752,13637,13638],{},"Install Whisper via pip:",[929,13640,13642],{"className":4657,"code":13641,"language":4659,"meta":934,"style":934},"pip install openai-whisper\n",[920,13643,13644],{"__ignoreMap":934},[938,13645,13646,13649,13651],{"class":940,"line":941},[938,13647,13648],{"class":1020},"pip",[938,13650,13567],{"class":1727},[938,13652,13653],{"class":1727}," openai-whisper\n",[752,13655,13656],{},"For faster GPU transcription (NVIDIA):",[929,13658,13660],{"className":4657,"code":13659,"language":4659,"meta":934,"style":934},"pip install openai-whisper torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118\n",[920,13661,13662],{"__ignoreMap":934},[938,13663,13664,13666,13668,13671,13674,13677,13680,13683],{"class":940,"line":941},[938,13665,13648],{"class":1020},[938,13667,13567],{"class":1727},[938,13669,13670],{"class":1727}," openai-whisper",[938,13672,13673],{"class":1727}," torch",[938,13675,13676],{"class":1727}," torchvision",[938,13678,13679],{"class":1727}," torchaudio",[938,13681,13682],{"class":1727}," --index-url",[938,13684,13685],{"class":1727}," https://download.pytorch.org/whl/cu118\n",[767,13687],{},[747,13689,13691],{"id":13690},"step-3-extract-audio-from-video","Step 3: Extract Audio from Video",[752,13693,13694],{},"Pull the audio track from your video file:",[929,13696,13698],{"className":4657,"code":13697,"language":4659,"meta":934,"style":934},"ffmpeg -i interview.mp4 -vn -acodec libmp3lame -q:a 2 interview.mp3\n",[920,13699,13700],{"__ignoreMap":934},[938,13701,13702,13704,13707,13710,13713,13716,13719,13722,13725],{"class":940,"line":941},[938,13703,13626],{"class":1020},[938,13705,13706],{"class":1727}," -i",[938,13708,13709],{"class":1727}," interview.mp4",[938,13711,13712],{"class":1727}," -vn",[938,13714,13715],{"class":1727}," -acodec",[938,13717,13718],{"class":1727}," libmp3lame",[938,13720,13721],{"class":1727}," -q:a",[938,13723,13724],{"class":1522}," 2",[938,13726,13727],{"class":1727}," interview.mp3\n",[752,13729,13730],{},"Flag breakdown:",[779,13732,13733,13739,13745,13751],{},[782,13734,13735,13738],{},[920,13736,13737],{},"-i interview.mp4"," - input file",[782,13740,13741,13744],{},[920,13742,13743],{},"-vn"," - no video (audio only)",[782,13746,13747,13750],{},[920,13748,13749],{},"-acodec libmp3lame"," - MP3 codec",[782,13752,13753,13756],{},[920,13754,13755],{},"-q:a 2"," - high audio quality",[752,13758,13759],{},"For a smaller file (sufficient for transcription):",[929,13761,13763],{"className":4657,"code":13762,"language":4659,"meta":934,"style":934},"ffmpeg -i interview.mp4 -vn -ar 16000 -ac 1 -b:a 64k interview.mp3\n",[920,13764,13765],{"__ignoreMap":934},[938,13766,13767,13769,13771,13773,13775,13778,13781,13784,13786,13789,13792],{"class":940,"line":941},[938,13768,13626],{"class":1020},[938,13770,13706],{"class":1727},[938,13772,13709],{"class":1727},[938,13774,13712],{"class":1727},[938,13776,13777],{"class":1727}," -ar",[938,13779,13780],{"class":1522}," 16000",[938,13782,13783],{"class":1727}," -ac",[938,13785,9196],{"class":1522},[938,13787,13788],{"class":1727}," -b:a",[938,13790,13791],{"class":1727}," 64k",[938,13793,13727],{"class":1727},[767,13795],{},[747,13797,13799],{"id":13798},"step-4-transcribe-with-whisper","Step 4: Transcribe with Whisper",[774,13801,13803],{"id":13802},"basic-cli-usage","Basic CLI Usage",[929,13805,13807],{"className":4657,"code":13806,"language":4659,"meta":934,"style":934},"whisper interview.mp3 --language English --model medium\n",[920,13808,13809],{"__ignoreMap":934},[938,13810,13811,13814,13817,13820,13823,13826],{"class":940,"line":941},[938,13812,13813],{"class":1020},"whisper",[938,13815,13816],{"class":1727}," interview.mp3",[938,13818,13819],{"class":1727}," --language",[938,13821,13822],{"class":1727}," English",[938,13824,13825],{"class":1727}," --model",[938,13827,13828],{"class":1727}," medium\n",[774,13830,13832],{"id":13831},"available-models","Available Models",[1906,13834,13835,13853],{},[1909,13836,13837],{},[1912,13838,13839,13841,13844,13847,13850],{},[1915,13840,7634],{},[1915,13842,13843],{},"Size",[1915,13845,13846],{},"VRAM",[1915,13848,13849],{},"Quality",[1915,13851,13852],{},"Speed",[1922,13854,13855,13872,13888,13904,13921],{},[1912,13856,13857,13860,13863,13866,13869],{},[1927,13858,13859],{},"tiny",[1927,13861,13862],{},"39 MB",[1927,13864,13865],{},"~1 GB",[1927,13867,13868],{},"Low",[1927,13870,13871],{},"Very fast",[1912,13873,13874,13877,13880,13882,13885],{},[1927,13875,13876],{},"base",[1927,13878,13879],{},"74 MB",[1927,13881,13865],{},[1927,13883,13884],{},"Medium",[1927,13886,13887],{},"Fast",[1912,13889,13890,13893,13896,13899,13902],{},[1927,13891,13892],{},"small",[1927,13894,13895],{},"244 MB",[1927,13897,13898],{},"~2 GB",[1927,13900,13901],{},"Good",[1927,13903,13884],{},[1912,13905,13906,13909,13912,13915,13918],{},[1927,13907,13908],{},"medium",[1927,13910,13911],{},"769 MB",[1927,13913,13914],{},"~5 GB",[1927,13916,13917],{},"Very good",[1927,13919,13920],{},"Slow",[1912,13922,13923,13926,13929,13932,13935],{},[1927,13924,13925],{},"large",[1927,13927,13928],{},"1550 MB",[1927,13930,13931],{},"~10 GB",[1927,13933,13934],{},"Best",[1927,13936,13937],{},"Very slow",[752,13939,13940,13941,923,13943,13945],{},"For most use cases, ",[920,13942,13892],{},[920,13944,13908],{}," offers the best quality-to-speed ratio.",[774,13947,13949],{"id":13948},"save-to-text-file","Save to Text File",[929,13951,13953],{"className":4657,"code":13952,"language":4659,"meta":934,"style":934},"whisper interview.mp3 --language English --model medium --output_format txt\n",[920,13954,13955],{"__ignoreMap":934},[938,13956,13957,13959,13961,13963,13965,13967,13970,13973],{"class":940,"line":941},[938,13958,13813],{"class":1020},[938,13960,13816],{"class":1727},[938,13962,13819],{"class":1727},[938,13964,13822],{"class":1727},[938,13966,13825],{"class":1727},[938,13968,13969],{"class":1727}," medium",[938,13971,13972],{"class":1727}," --output_format",[938,13974,13975],{"class":1727}," txt\n",[752,13977,13978,13979,9612,13982,9612,13985,9612,13988,9612,13991],{},"Available output formats: ",[920,13980,13981],{},"txt",[920,13983,13984],{},"vtt",[920,13986,13987],{},"srt",[920,13989,13990],{},"tsv",[920,13992,2754],{},[767,13994],{},[747,13996,13998],{"id":13997},"example-transcribing-a-user-interview","Example: Transcribing a User Interview",[752,14000,14001],{},"Let's say you recorded a 15-minute user feedback session about your product. Here's the complete workflow:",[774,14003,14005],{"id":14004},"_1-extract-audio","1. Extract Audio",[929,14007,14009],{"className":4657,"code":14008,"language":4659,"meta":934,"style":934},"ffmpeg -i user_feedback_session.mov -vn -ar 16000 -ac 1 feedback.mp3\n",[920,14010,14011],{"__ignoreMap":934},[938,14012,14013,14015,14017,14020,14022,14024,14026,14028,14030],{"class":940,"line":941},[938,14014,13626],{"class":1020},[938,14016,13706],{"class":1727},[938,14018,14019],{"class":1727}," user_feedback_session.mov",[938,14021,13712],{"class":1727},[938,14023,13777],{"class":1727},[938,14025,13780],{"class":1522},[938,14027,13783],{"class":1727},[938,14029,9196],{"class":1522},[938,14031,14032],{"class":1727}," feedback.mp3\n",[774,14034,14036],{"id":14035},"_2-run-transcription","2. Run Transcription",[929,14038,14040],{"className":4657,"code":14039,"language":4659,"meta":934,"style":934},"whisper feedback.mp3 --language English --model medium --output_format txt\n",[920,14041,14042],{"__ignoreMap":934},[938,14043,14044,14046,14049,14051,14053,14055,14057,14059],{"class":940,"line":941},[938,14045,13813],{"class":1020},[938,14047,14048],{"class":1727}," feedback.mp3",[938,14050,13819],{"class":1727},[938,14052,13822],{"class":1727},[938,14054,13825],{"class":1727},[938,14056,13969],{"class":1727},[938,14058,13972],{"class":1727},[938,14060,13975],{"class":1727},[774,14062,14064],{"id":14063},"_3-output","3. Output",[752,14066,14067,14068,14071],{},"Whisper creates ",[920,14069,14070],{},"feedback.txt"," with content like:",[929,14073,14076],{"className":14074,"code":14075,"language":8289},[8287],"So the first thing about these signals before the interval.\nYes, so you set a 25-second interval and before the interval\nyou get 4 audio signals, let's say, the main signal with an\naccent and then 4 weaker ones again. So basically if you don't\narrive exactly on the signal, you know that you need to speed\nup by two seconds or slow down by two seconds because the sound\ntells you that.\n",[920,14077,14075],{"__ignoreMap":934},[767,14079],{},[747,14081,14083],{"id":14082},"example-python-script-for-batch-processing","Example: Python Script for Batch Processing",[752,14085,14086],{},"For processing multiple files or integrating into your workflow:",[929,14088,14090],{"className":2609,"code":14089,"language":2611,"meta":934,"style":934},"#!/usr/bin/env python3\n\"\"\"\nVideo to Text Transcription Pipeline\nUses FFmpeg + OpenAI Whisper\n\"\"\"\n\nimport subprocess\nimport whisper\nfrom pathlib import Path\n\n\ndef extract_audio(video_path: str, audio_path: str) -> None:\n    \"\"\"Extract audio from video using FFmpeg.\"\"\"\n    cmd = [\n        \"ffmpeg\", \"-i\", video_path,\n        \"-vn\", \"-ar\", \"16000\", \"-ac\", \"1\",\n        \"-b:a\", \"64k\", \"-y\",\n        audio_path\n    ]\n    subprocess.run(cmd, check=True, capture_output=True)\n    print(f\"Audio extracted: {audio_path}\")\n\n\ndef transcribe_audio(\n    audio_path: str,\n    output_path: str,\n    model_name: str = \"medium\",\n    language: str = \"en\"\n) -> str:\n    \"\"\"Transcribe audio using Whisper.\"\"\"\n    print(f\"Loading model {model_name}...\")\n    model = whisper.load_model(model_name)\n\n    print(\"Transcribing...\")\n    result = model.transcribe(audio_path, language=language)\n\n    text = result[\"text\"]\n\n    with open(output_path, \"w\", encoding=\"utf-8\") as f:\n        f.write(text)\n\n    print(f\"Transcription saved: {output_path}\")\n    return text\n\n\ndef main():\n    # Configuration\n    video_file = \"meeting_recording.mp4\"\n    audio_file = \"temp_audio.mp3\"\n    output_file = \"meeting_transcript.txt\"\n\n    # Pipeline\n    extract_audio(video_file, audio_file)\n    text = transcribe_audio(audio_file, output_file)\n\n    # Cleanup temp file\n    Path(audio_file).unlink()\n\n    print(f\"\\nTranscription preview:\\n{text[:500]}...\")\n\n\nif __name__ == \"__main__\":\n    main()\n",[920,14091,14092,14097,14101,14106,14111,14115,14119,14126,14133,14145,14149,14153,14184,14193,14202,14226,14271,14300,14305,14309,14336,14359,14363,14367,14376,14387,14398,14417,14435,14445,14454,14477,14498,14502,14517,14548,14552,14571,14575,14617,14633,14637,14658,14666,14670,14674,14684,14689,14703,14717,14731,14735,14740,14757,14777,14781,14786,14802,14806,14842,14846,14850,14869],{"__ignoreMap":934},[938,14093,14094],{"class":940,"line":941},[938,14095,14096],{"class":1535},"#!/usr/bin/env python3\n",[938,14098,14099],{"class":940,"line":967},[938,14100,5078],{"class":1097},[938,14102,14103],{"class":940,"line":1053},[938,14104,14105],{"class":1535},"Video to Text Transcription Pipeline\n",[938,14107,14108],{"class":940,"line":1060},[938,14109,14110],{"class":1535},"Uses FFmpeg + OpenAI Whisper\n",[938,14112,14113],{"class":940,"line":1066},[938,14114,5078],{"class":1097},[938,14116,14117],{"class":940,"line":1094},[938,14118,1057],{"emptyLinePlaceholder":1056},[938,14120,14121,14123],{"class":940,"line":1116},[938,14122,2618],{"class":1097},[938,14124,14125],{"class":955}," subprocess\n",[938,14127,14128,14130],{"class":940,"line":1134},[938,14129,2618],{"class":1097},[938,14131,14132],{"class":955}," whisper\n",[938,14134,14135,14137,14140,14142],{"class":940,"line":1154},[938,14136,4984],{"class":1097},[938,14138,14139],{"class":955}," pathlib ",[938,14141,2618],{"class":1097},[938,14143,14144],{"class":955}," Path\n",[938,14146,14147],{"class":940,"line":1171},[938,14148,1057],{"emptyLinePlaceholder":1056},[938,14150,14151],{"class":940,"line":1191},[938,14152,1057],{"emptyLinePlaceholder":1056},[938,14154,14155,14157,14160,14162,14165,14167,14169,14171,14174,14176,14178,14180,14182],{"class":940,"line":1196},[938,14156,5958],{"class":1035},[938,14158,14159],{"class":1072}," extract_audio",[938,14161,1076],{"class":944},[938,14163,14164],{"class":2690},"video_path",[938,14166,1880],{"class":944},[938,14168,5457],{"class":1020},[938,14170,1085],{"class":944},[938,14172,14173],{"class":2690}," audio_path",[938,14175,1880],{"class":944},[938,14177,5457],{"class":1020},[938,14179,1731],{"class":944},[938,14181,5159],{"class":944},[938,14183,5463],{"class":944},[938,14185,14186,14188,14191],{"class":940,"line":1231},[938,14187,5072],{"class":1097},[938,14189,14190],{"class":1535},"Extract audio from video using FFmpeg.",[938,14192,5078],{"class":1097},[938,14194,14195,14198,14200],{"class":940,"line":1237},[938,14196,14197],{"class":955},"    cmd ",[938,14199,1125],{"class":944},[938,14201,2909],{"class":944},[938,14203,14204,14206,14208,14210,14212,14214,14217,14219,14221,14224],{"class":940,"line":1243},[938,14205,3052],{"class":944},[938,14207,13626],{"class":1727},[938,14209,2673],{"class":944},[938,14211,1085],{"class":944},[938,14213,2635],{"class":944},[938,14215,14216],{"class":1727},"-i",[938,14218,2673],{"class":944},[938,14220,1085],{"class":944},[938,14222,14223],{"class":955}," video_path",[938,14225,2716],{"class":944},[938,14227,14228,14230,14232,14234,14236,14238,14241,14243,14245,14247,14250,14252,14254,14256,14259,14261,14263,14265,14267,14269],{"class":940,"line":2431},[938,14229,3052],{"class":944},[938,14231,13743],{"class":1727},[938,14233,2673],{"class":944},[938,14235,1085],{"class":944},[938,14237,2635],{"class":944},[938,14239,14240],{"class":1727},"-ar",[938,14242,2673],{"class":944},[938,14244,1085],{"class":944},[938,14246,2635],{"class":944},[938,14248,14249],{"class":1727},"16000",[938,14251,2673],{"class":944},[938,14253,1085],{"class":944},[938,14255,2635],{"class":944},[938,14257,14258],{"class":1727},"-ac",[938,14260,2673],{"class":944},[938,14262,1085],{"class":944},[938,14264,2635],{"class":944},[938,14266,8744],{"class":1727},[938,14268,2673],{"class":944},[938,14270,2716],{"class":944},[938,14272,14273,14275,14278,14280,14282,14284,14287,14289,14291,14293,14296,14298],{"class":940,"line":2437},[938,14274,3052],{"class":944},[938,14276,14277],{"class":1727},"-b:a",[938,14279,2673],{"class":944},[938,14281,1085],{"class":944},[938,14283,2635],{"class":944},[938,14285,14286],{"class":1727},"64k",[938,14288,2673],{"class":944},[938,14290,1085],{"class":944},[938,14292,2635],{"class":944},[938,14294,14295],{"class":1727},"-y",[938,14297,2673],{"class":944},[938,14299,2716],{"class":944},[938,14301,14302],{"class":940,"line":2443},[938,14303,14304],{"class":955},"        audio_path\n",[938,14306,14307],{"class":940,"line":2449},[938,14308,13136],{"class":944},[938,14310,14311,14314,14316,14319,14321,14324,14326,14329,14331,14334],{"class":940,"line":2454},[938,14312,14313],{"class":955},"    subprocess",[938,14315,1148],{"class":944},[938,14317,14318],{"class":1072},"run",[938,14320,1076],{"class":944},[938,14322,14323],{"class":1072},"cmd",[938,14325,1085],{"class":944},[938,14327,14328],{"class":2690}," check",[938,14330,7938],{"class":944},[938,14332,14333],{"class":2690}," capture_output",[938,14335,7944],{"class":944},[938,14337,14338,14341,14343,14345,14348,14350,14353,14355,14357],{"class":940,"line":2460},[938,14339,14340],{"class":1072},"    print",[938,14342,1076],{"class":944},[938,14344,2670],{"class":1035},[938,14346,14347],{"class":1727},"\"Audio extracted: ",[938,14349,2676],{"class":1522},[938,14351,14352],{"class":1072},"audio_path",[938,14354,2682],{"class":1522},[938,14356,2673],{"class":1727},[938,14358,3256],{"class":944},[938,14360,14361],{"class":940,"line":2466},[938,14362,1057],{"emptyLinePlaceholder":1056},[938,14364,14365],{"class":940,"line":2472},[938,14366,1057],{"emptyLinePlaceholder":1056},[938,14368,14369,14371,14374],{"class":940,"line":3049},[938,14370,5958],{"class":1035},[938,14372,14373],{"class":1072}," transcribe_audio",[938,14375,3210],{"class":944},[938,14377,14378,14381,14383,14385],{"class":940,"line":3071},[938,14379,14380],{"class":2690},"    audio_path",[938,14382,1880],{"class":944},[938,14384,5457],{"class":1020},[938,14386,2716],{"class":944},[938,14388,14389,14392,14394,14396],{"class":940,"line":3149},[938,14390,14391],{"class":2690},"    output_path",[938,14393,1880],{"class":944},[938,14395,5457],{"class":1020},[938,14397,2716],{"class":944},[938,14399,14400,14403,14405,14407,14409,14411,14413,14415],{"class":940,"line":3154},[938,14401,14402],{"class":2690},"    model_name",[938,14404,1880],{"class":944},[938,14406,5457],{"class":1020},[938,14408,5214],{"class":944},[938,14410,2635],{"class":944},[938,14412,13908],{"class":1727},[938,14414,2673],{"class":944},[938,14416,2716],{"class":944},[938,14418,14419,14422,14424,14426,14428,14430,14433],{"class":940,"line":3159},[938,14420,14421],{"class":2690},"    language",[938,14423,1880],{"class":944},[938,14425,5457],{"class":1020},[938,14427,5214],{"class":944},[938,14429,2635],{"class":944},[938,14431,14432],{"class":1727},"en",[938,14434,2641],{"class":944},[938,14436,14437,14439,14441,14443],{"class":940,"line":3185},[938,14438,1731],{"class":944},[938,14440,5159],{"class":944},[938,14442,5457],{"class":1020},[938,14444,5067],{"class":944},[938,14446,14447,14449,14452],{"class":940,"line":3190},[938,14448,5072],{"class":1097},[938,14450,14451],{"class":1535},"Transcribe audio using Whisper.",[938,14453,5078],{"class":1097},[938,14455,14456,14458,14460,14462,14465,14467,14470,14472,14475],{"class":940,"line":3196},[938,14457,14340],{"class":1072},[938,14459,1076],{"class":944},[938,14461,2670],{"class":1035},[938,14463,14464],{"class":1727},"\"Loading model ",[938,14466,2676],{"class":1522},[938,14468,14469],{"class":1072},"model_name",[938,14471,2682],{"class":1522},[938,14473,14474],{"class":1727},"...\"",[938,14476,3256],{"class":944},[938,14478,14479,14482,14484,14487,14489,14492,14494,14496],{"class":940,"line":3213},[938,14480,14481],{"class":955},"    model ",[938,14483,1125],{"class":944},[938,14485,14486],{"class":955}," whisper",[938,14488,1148],{"class":944},[938,14490,14491],{"class":1072},"load_model",[938,14493,1076],{"class":944},[938,14495,14469],{"class":1072},[938,14497,3256],{"class":944},[938,14499,14500],{"class":940,"line":3242},[938,14501,1057],{"emptyLinePlaceholder":1056},[938,14503,14504,14506,14508,14510,14513,14515],{"class":940,"line":3253},[938,14505,14340],{"class":1072},[938,14507,1076],{"class":944},[938,14509,2673],{"class":944},[938,14511,14512],{"class":1727},"Transcribing...",[938,14514,2673],{"class":944},[938,14516,3256],{"class":944},[938,14518,14519,14522,14524,14527,14529,14532,14534,14536,14538,14541,14543,14546],{"class":940,"line":3259},[938,14520,14521],{"class":955},"    result ",[938,14523,1125],{"class":944},[938,14525,14526],{"class":955}," model",[938,14528,1148],{"class":944},[938,14530,14531],{"class":1072},"transcribe",[938,14533,1076],{"class":944},[938,14535,14352],{"class":1072},[938,14537,1085],{"class":944},[938,14539,14540],{"class":2690}," language",[938,14542,1125],{"class":944},[938,14544,14545],{"class":1072},"language",[938,14547,3256],{"class":944},[938,14549,14550],{"class":940,"line":3264},[938,14551,1057],{"emptyLinePlaceholder":1056},[938,14553,14554,14557,14559,14561,14563,14565,14567,14569],{"class":940,"line":3270},[938,14555,14556],{"class":955},"    text ",[938,14558,1125],{"class":944},[938,14560,8805],{"class":955},[938,14562,5800],{"class":944},[938,14564,2673],{"class":944},[938,14566,8289],{"class":1727},[938,14568,2673],{"class":944},[938,14570,2767],{"class":944},[938,14572,14573],{"class":940,"line":3294},[938,14574,1057],{"emptyLinePlaceholder":1056},[938,14576,14577,14579,14582,14584,14587,14589,14591,14594,14596,14598,14601,14603,14605,14607,14609,14611,14613,14615],{"class":940,"line":3321},[938,14578,6047],{"class":1097},[938,14580,14581],{"class":1072}," open",[938,14583,1076],{"class":944},[938,14585,14586],{"class":1072},"output_path",[938,14588,1085],{"class":944},[938,14590,2635],{"class":944},[938,14592,14593],{"class":1727},"w",[938,14595,2673],{"class":944},[938,14597,1085],{"class":944},[938,14599,14600],{"class":2690}," encoding",[938,14602,1125],{"class":944},[938,14604,2673],{"class":944},[938,14606,9890],{"class":1727},[938,14608,2673],{"class":944},[938,14610,1731],{"class":944},[938,14612,5543],{"class":1097},[938,14614,2789],{"class":955},[938,14616,5067],{"class":944},[938,14618,14619,14622,14624,14627,14629,14631],{"class":940,"line":3333},[938,14620,14621],{"class":955},"        f",[938,14623,1148],{"class":944},[938,14625,14626],{"class":1072},"write",[938,14628,1076],{"class":944},[938,14630,8289],{"class":1072},[938,14632,3256],{"class":944},[938,14634,14635],{"class":940,"line":3356},[938,14636,1057],{"emptyLinePlaceholder":1056},[938,14638,14639,14641,14643,14645,14648,14650,14652,14654,14656],{"class":940,"line":5513},[938,14640,14340],{"class":1072},[938,14642,1076],{"class":944},[938,14644,2670],{"class":1035},[938,14646,14647],{"class":1727},"\"Transcription saved: ",[938,14649,2676],{"class":1522},[938,14651,14586],{"class":1072},[938,14653,2682],{"class":1522},[938,14655,2673],{"class":1727},[938,14657,3256],{"class":944},[938,14659,14660,14663],{"class":940,"line":5518},[938,14661,14662],{"class":1097},"    return",[938,14664,14665],{"class":955}," text\n",[938,14667,14668],{"class":940,"line":5551},[938,14669,1057],{"emptyLinePlaceholder":1056},[938,14671,14672],{"class":940,"line":5567},[938,14673,1057],{"emptyLinePlaceholder":1056},[938,14675,14676,14678,14681],{"class":940,"line":5590},[938,14677,5958],{"class":1035},[938,14679,14680],{"class":1072}," main",[938,14682,14683],{"class":944},"():\n",[938,14685,14686],{"class":940,"line":5598},[938,14687,14688],{"class":1535},"    # Configuration\n",[938,14690,14691,14694,14696,14698,14701],{"class":940,"line":5619},[938,14692,14693],{"class":955},"    video_file ",[938,14695,1125],{"class":944},[938,14697,2635],{"class":944},[938,14699,14700],{"class":1727},"meeting_recording.mp4",[938,14702,2641],{"class":944},[938,14704,14705,14708,14710,14712,14715],{"class":940,"line":5639},[938,14706,14707],{"class":955},"    audio_file ",[938,14709,1125],{"class":944},[938,14711,2635],{"class":944},[938,14713,14714],{"class":1727},"temp_audio.mp3",[938,14716,2641],{"class":944},[938,14718,14719,14722,14724,14726,14729],{"class":940,"line":5658},[938,14720,14721],{"class":955},"    output_file ",[938,14723,1125],{"class":944},[938,14725,2635],{"class":944},[938,14727,14728],{"class":1727},"meeting_transcript.txt",[938,14730,2641],{"class":944},[938,14732,14733],{"class":940,"line":5678},[938,14734,1057],{"emptyLinePlaceholder":1056},[938,14736,14737],{"class":940,"line":5684},[938,14738,14739],{"class":1535},"    # Pipeline\n",[938,14741,14742,14745,14747,14750,14752,14755],{"class":940,"line":5692},[938,14743,14744],{"class":1072},"    extract_audio",[938,14746,1076],{"class":944},[938,14748,14749],{"class":1072},"video_file",[938,14751,1085],{"class":944},[938,14753,14754],{"class":1072}," audio_file",[938,14756,3256],{"class":944},[938,14758,14759,14761,14763,14765,14767,14770,14772,14775],{"class":940,"line":5722},[938,14760,14556],{"class":955},[938,14762,1125],{"class":944},[938,14764,14373],{"class":1072},[938,14766,1076],{"class":944},[938,14768,14769],{"class":1072},"audio_file",[938,14771,1085],{"class":944},[938,14773,14774],{"class":1072}," output_file",[938,14776,3256],{"class":944},[938,14778,14779],{"class":940,"line":5743},[938,14780,1057],{"emptyLinePlaceholder":1056},[938,14782,14783],{"class":940,"line":5748},[938,14784,14785],{"class":1535},"    # Cleanup temp file\n",[938,14787,14788,14791,14793,14795,14797,14800],{"class":940,"line":5754},[938,14789,14790],{"class":1072},"    Path",[938,14792,1076],{"class":944},[938,14794,14769],{"class":1072},[938,14796,8637],{"class":944},[938,14798,14799],{"class":1072},"unlink",[938,14801,1131],{"class":944},[938,14803,14804],{"class":940,"line":5767},[938,14805,1057],{"emptyLinePlaceholder":1056},[938,14807,14808,14810,14812,14814,14816,14819,14822,14824,14826,14828,14831,14834,14836,14838,14840],{"class":940,"line":5784},[938,14809,14340],{"class":1072},[938,14811,1076],{"class":944},[938,14813,2670],{"class":1035},[938,14815,2673],{"class":1727},[938,14817,14818],{"class":955},"\\n",[938,14820,14821],{"class":1727},"Transcription preview:",[938,14823,14818],{"class":955},[938,14825,2676],{"class":1522},[938,14827,8289],{"class":1072},[938,14829,14830],{"class":944},"[:",[938,14832,14833],{"class":1522},"500",[938,14835,6931],{"class":944},[938,14837,2682],{"class":1522},[938,14839,14474],{"class":1727},[938,14841,3256],{"class":944},[938,14843,14844],{"class":940,"line":5789},[938,14845,1057],{"emptyLinePlaceholder":1056},[938,14847,14848],{"class":940,"line":5812},[938,14849,1057],{"emptyLinePlaceholder":1056},[938,14851,14852,14854,14857,14860,14862,14865,14867],{"class":940,"line":5842},[938,14853,1676],{"class":1097},[938,14855,14856],{"class":955}," __name__ ",[938,14858,14859],{"class":944},"==",[938,14861,2635],{"class":944},[938,14863,14864],{"class":1727},"__main__",[938,14866,2673],{"class":944},[938,14868,5067],{"class":944},[938,14870,14871,14874],{"class":940,"line":5870},[938,14872,14873],{"class":1072},"    main",[938,14875,1131],{"class":944},[752,14877,14878],{},"Run it:",[929,14880,14882],{"className":4657,"code":14881,"language":4659,"meta":934,"style":934},"python transcribe.py\n",[920,14883,14884],{"__ignoreMap":934},[938,14885,14886,14888],{"class":940,"line":941},[938,14887,2611],{"class":1020},[938,14889,14890],{"class":1727}," transcribe.py\n",[767,14892],{},[747,14894,14896],{"id":14895},"example-transcription-with-timestamps","Example: Transcription with Timestamps",[752,14898,14899],{},"Need timestamps for subtitles or reference? Use the segments feature:",[929,14901,14903],{"className":2609,"code":14902,"language":2611,"meta":934,"style":934},"import whisper\n\nmodel = whisper.load_model(\"medium\")\nresult = model.transcribe(\"podcast_episode.mp3\", language=\"en\")\n\n# Print segments with timestamps\nfor segment in result[\"segments\"]:\n    start = segment[\"start\"]\n    end = segment[\"end\"]\n    text = segment[\"text\"].strip()\n    print(f\"[{start:.1f}s - {end:.1f}s] {text}\")\n",[920,14904,14905,14911,14915,14938,14974,14978,14983,15005,15025,15044,15068],{"__ignoreMap":934},[938,14906,14907,14909],{"class":940,"line":941},[938,14908,2618],{"class":1097},[938,14910,14132],{"class":955},[938,14912,14913],{"class":940,"line":967},[938,14914,1057],{"emptyLinePlaceholder":1056},[938,14916,14917,14920,14922,14924,14926,14928,14930,14932,14934,14936],{"class":940,"line":1053},[938,14918,14919],{"class":955},"model ",[938,14921,1125],{"class":944},[938,14923,14486],{"class":955},[938,14925,1148],{"class":944},[938,14927,14491],{"class":1072},[938,14929,1076],{"class":944},[938,14931,2673],{"class":944},[938,14933,13908],{"class":1727},[938,14935,2673],{"class":944},[938,14937,3256],{"class":944},[938,14939,14940,14943,14945,14947,14949,14951,14953,14955,14958,14960,14962,14964,14966,14968,14970,14972],{"class":940,"line":1060},[938,14941,14942],{"class":955},"result ",[938,14944,1125],{"class":944},[938,14946,14526],{"class":955},[938,14948,1148],{"class":944},[938,14950,14531],{"class":1072},[938,14952,1076],{"class":944},[938,14954,2673],{"class":944},[938,14956,14957],{"class":1727},"podcast_episode.mp3",[938,14959,2673],{"class":944},[938,14961,1085],{"class":944},[938,14963,14540],{"class":2690},[938,14965,1125],{"class":944},[938,14967,2673],{"class":944},[938,14969,14432],{"class":1727},[938,14971,2673],{"class":944},[938,14973,3256],{"class":944},[938,14975,14976],{"class":940,"line":1066},[938,14977,1057],{"emptyLinePlaceholder":1056},[938,14979,14980],{"class":940,"line":1094},[938,14981,14982],{"class":1535},"# Print segments with timestamps\n",[938,14984,14985,14987,14990,14992,14994,14996,14998,15001,15003],{"class":940,"line":1116},[938,14986,9063],{"class":1097},[938,14988,14989],{"class":955}," segment ",[938,14991,1110],{"class":1097},[938,14993,8805],{"class":955},[938,14995,5800],{"class":944},[938,14997,2673],{"class":944},[938,14999,15000],{"class":1727},"segments",[938,15002,2673],{"class":944},[938,15004,3291],{"class":944},[938,15006,15007,15010,15012,15015,15017,15019,15021,15023],{"class":940,"line":1134},[938,15008,15009],{"class":955},"    start ",[938,15011,1125],{"class":944},[938,15013,15014],{"class":955}," segment",[938,15016,5800],{"class":944},[938,15018,2673],{"class":944},[938,15020,1212],{"class":1727},[938,15022,2673],{"class":944},[938,15024,2767],{"class":944},[938,15026,15027,15030,15032,15034,15036,15038,15040,15042],{"class":940,"line":1154},[938,15028,15029],{"class":955},"    end ",[938,15031,1125],{"class":944},[938,15033,15014],{"class":955},[938,15035,5800],{"class":944},[938,15037,2673],{"class":944},[938,15039,1221],{"class":1727},[938,15041,2673],{"class":944},[938,15043,2767],{"class":944},[938,15045,15046,15048,15050,15052,15054,15056,15058,15060,15063,15066],{"class":940,"line":1171},[938,15047,14556],{"class":955},[938,15049,1125],{"class":944},[938,15051,15014],{"class":955},[938,15053,5800],{"class":944},[938,15055,2673],{"class":944},[938,15057,8289],{"class":1727},[938,15059,2673],{"class":944},[938,15061,15062],{"class":944},"].",[938,15064,15065],{"class":1072},"strip",[938,15067,1131],{"class":944},[938,15069,15070,15072,15074,15076,15079,15081,15083,15086,15088,15091,15093,15095,15097,15099,15102,15104,15106,15108,15110],{"class":940,"line":1191},[938,15071,14340],{"class":1072},[938,15073,1076],{"class":944},[938,15075,2670],{"class":1035},[938,15077,15078],{"class":1727},"\"[",[938,15080,2676],{"class":1522},[938,15082,1212],{"class":1072},[938,15084,15085],{"class":1035},":.1f",[938,15087,2682],{"class":1522},[938,15089,15090],{"class":1727},"s - ",[938,15092,2676],{"class":1522},[938,15094,1221],{"class":1072},[938,15096,15085],{"class":1035},[938,15098,2682],{"class":1522},[938,15100,15101],{"class":1727},"s] ",[938,15103,2676],{"class":1522},[938,15105,8289],{"class":1072},[938,15107,2682],{"class":1522},[938,15109,2673],{"class":1727},[938,15111,3256],{"class":944},[752,15113,15114],{},"Output:",[929,15116,15119],{"className":15117,"code":15118,"language":8289},[8287],"[0.0s - 4.2s] Welcome to the show. Today we're talking about...\n[4.2s - 8.7s] ...building hardware products for athletes.\n[8.7s - 15.3s] Our guest has been working on a tempo trainer device.\n",[920,15120,15118],{"__ignoreMap":934},[767,15122],{},[747,15124,15126],{"id":15125},"example-multi-language-detection","Example: Multi-Language Detection",[752,15128,15129],{},"Don't know the language? Let Whisper detect it:",[929,15131,15133],{"className":2609,"code":15132,"language":2611,"meta":934,"style":934},"import whisper\n\nmodel = whisper.load_model(\"medium\")\n\n# Auto-detect language\nresult = model.transcribe(\"unknown_language.mp3\")\n\nprint(f\"Detected language: {result['language']}\")\nprint(f\"Text: {result['text']}\")\n",[920,15134,15135,15141,15145,15167,15171,15176,15199,15203,15235],{"__ignoreMap":934},[938,15136,15137,15139],{"class":940,"line":941},[938,15138,2618],{"class":1097},[938,15140,14132],{"class":955},[938,15142,15143],{"class":940,"line":967},[938,15144,1057],{"emptyLinePlaceholder":1056},[938,15146,15147,15149,15151,15153,15155,15157,15159,15161,15163,15165],{"class":940,"line":1053},[938,15148,14919],{"class":955},[938,15150,1125],{"class":944},[938,15152,14486],{"class":955},[938,15154,1148],{"class":944},[938,15156,14491],{"class":1072},[938,15158,1076],{"class":944},[938,15160,2673],{"class":944},[938,15162,13908],{"class":1727},[938,15164,2673],{"class":944},[938,15166,3256],{"class":944},[938,15168,15169],{"class":940,"line":1060},[938,15170,1057],{"emptyLinePlaceholder":1056},[938,15172,15173],{"class":940,"line":1066},[938,15174,15175],{"class":1535},"# Auto-detect language\n",[938,15177,15178,15180,15182,15184,15186,15188,15190,15192,15195,15197],{"class":940,"line":1094},[938,15179,14942],{"class":955},[938,15181,1125],{"class":944},[938,15183,14526],{"class":955},[938,15185,1148],{"class":944},[938,15187,14531],{"class":1072},[938,15189,1076],{"class":944},[938,15191,2673],{"class":944},[938,15193,15194],{"class":1727},"unknown_language.mp3",[938,15196,2673],{"class":944},[938,15198,3256],{"class":944},[938,15200,15201],{"class":940,"line":1116},[938,15202,1057],{"emptyLinePlaceholder":1056},[938,15204,15205,15208,15210,15212,15215,15217,15219,15221,15223,15225,15227,15229,15231,15233],{"class":940,"line":1134},[938,15206,15207],{"class":1072},"print",[938,15209,1076],{"class":944},[938,15211,2670],{"class":1035},[938,15213,15214],{"class":1727},"\"Detected language: ",[938,15216,2676],{"class":1522},[938,15218,12803],{"class":1072},[938,15220,5800],{"class":944},[938,15222,10753],{"class":944},[938,15224,14545],{"class":1727},[938,15226,10753],{"class":944},[938,15228,6931],{"class":944},[938,15230,2682],{"class":1522},[938,15232,2673],{"class":1727},[938,15234,3256],{"class":944},[938,15236,15237,15239,15241,15243,15246,15248,15250,15252,15254,15256,15258,15260,15262,15264],{"class":940,"line":1154},[938,15238,15207],{"class":1072},[938,15240,1076],{"class":944},[938,15242,2670],{"class":1035},[938,15244,15245],{"class":1727},"\"Text: ",[938,15247,2676],{"class":1522},[938,15249,12803],{"class":1072},[938,15251,5800],{"class":944},[938,15253,10753],{"class":944},[938,15255,8289],{"class":1727},[938,15257,10753],{"class":944},[938,15259,6931],{"class":944},[938,15261,2682],{"class":1522},[938,15263,2673],{"class":1727},[938,15265,3256],{"class":944},[767,15267],{},[747,15269,15271],{"id":15270},"tips-and-optimizations","Tips and Optimizations",[774,15273,15275],{"id":15274},"_1-faster-transcription-on-apple-silicon","1. Faster Transcription on Apple Silicon",[929,15277,15279],{"className":4657,"code":15278,"language":4659,"meta":934,"style":934},"pip install mlx-whisper\n",[920,15280,15281],{"__ignoreMap":934},[938,15282,15283,15285,15287],{"class":940,"line":941},[938,15284,13648],{"class":1020},[938,15286,13567],{"class":1727},[938,15288,15289],{"class":1727}," mlx-whisper\n",[929,15291,15293],{"className":2609,"code":15292,"language":2611,"meta":934,"style":934},"import mlx_whisper\n\nresult = mlx_whisper.transcribe(\n    \"audio.mp3\",\n    path_or_hf_repo=\"mlx-community/whisper-medium-mlx\"\n)\n",[920,15294,15295,15302,15306,15321,15332,15346],{"__ignoreMap":934},[938,15296,15297,15299],{"class":940,"line":941},[938,15298,2618],{"class":1097},[938,15300,15301],{"class":955}," mlx_whisper\n",[938,15303,15304],{"class":940,"line":967},[938,15305,1057],{"emptyLinePlaceholder":1056},[938,15307,15308,15310,15312,15315,15317,15319],{"class":940,"line":1053},[938,15309,14942],{"class":955},[938,15311,1125],{"class":944},[938,15313,15314],{"class":955}," mlx_whisper",[938,15316,1148],{"class":944},[938,15318,14531],{"class":1072},[938,15320,3210],{"class":944},[938,15322,15323,15325,15328,15330],{"class":940,"line":1060},[938,15324,2699],{"class":944},[938,15326,15327],{"class":1727},"audio.mp3",[938,15329,2673],{"class":944},[938,15331,2716],{"class":944},[938,15333,15334,15337,15339,15341,15344],{"class":940,"line":1066},[938,15335,15336],{"class":2690},"    path_or_hf_repo",[938,15338,1125],{"class":944},[938,15340,2673],{"class":944},[938,15342,15343],{"class":1727},"mlx-community/whisper-medium-mlx",[938,15345,2641],{"class":944},[938,15347,15348],{"class":940,"line":1094},[938,15349,3256],{"class":944},[774,15351,15353],{"id":15352},"_2-handling-long-recordings","2. Handling Long Recordings",[752,15355,15356],{},"For recordings over 1 hour, consider splitting:",[929,15358,15360],{"className":4657,"code":15359,"language":4659,"meta":934,"style":934},"ffmpeg -i long_recording.mp3 -f segment -segment_time 1800 -c copy chunk_%03d.mp3\n",[920,15361,15362],{"__ignoreMap":934},[938,15363,15364,15366,15368,15371,15374,15376,15379,15382,15385,15388],{"class":940,"line":941},[938,15365,13626],{"class":1020},[938,15367,13706],{"class":1727},[938,15369,15370],{"class":1727}," long_recording.mp3",[938,15372,15373],{"class":1727}," -f",[938,15375,15014],{"class":1727},[938,15377,15378],{"class":1727}," -segment_time",[938,15380,15381],{"class":1522}," 1800",[938,15383,15384],{"class":1727}," -c",[938,15386,15387],{"class":1727}," copy",[938,15389,15390],{"class":1727}," chunk_%03d.mp3\n",[774,15392,15394],{"id":15393},"_3-improving-quality-for-difficult-audio","3. Improving Quality for Difficult Audio",[779,15396,15397,15404],{},[782,15398,15399,15400,5923,15402,1731],{},"Use a larger model (",[920,15401,13925],{},[920,15403,13908],{},[782,15405,15406,15407,1880],{},"Add context with ",[920,15408,15409],{},"initial_prompt",[929,15411,15413],{"className":2609,"code":15412,"language":2611,"meta":934,"style":934},"result = model.transcribe(\n    \"audio.mp3\",\n    language=\"en\",\n    initial_prompt=\"This is a conversation about swimming training and interval timers.\"\n)\n",[920,15414,15415,15429,15439,15453,15467],{"__ignoreMap":934},[938,15416,15417,15419,15421,15423,15425,15427],{"class":940,"line":941},[938,15418,14942],{"class":955},[938,15420,1125],{"class":944},[938,15422,14526],{"class":955},[938,15424,1148],{"class":944},[938,15426,14531],{"class":1072},[938,15428,3210],{"class":944},[938,15430,15431,15433,15435,15437],{"class":940,"line":967},[938,15432,2699],{"class":944},[938,15434,15327],{"class":1727},[938,15436,2673],{"class":944},[938,15438,2716],{"class":944},[938,15440,15441,15443,15445,15447,15449,15451],{"class":940,"line":1053},[938,15442,14421],{"class":2690},[938,15444,1125],{"class":944},[938,15446,2673],{"class":944},[938,15448,14432],{"class":1727},[938,15450,2673],{"class":944},[938,15452,2716],{"class":944},[938,15454,15455,15458,15460,15462,15465],{"class":940,"line":1060},[938,15456,15457],{"class":2690},"    initial_prompt",[938,15459,1125],{"class":944},[938,15461,2673],{"class":944},[938,15463,15464],{"class":1727},"This is a conversation about swimming training and interval timers.",[938,15466,2641],{"class":944},[938,15468,15469],{"class":940,"line":1066},[938,15470,3256],{"class":944},[767,15472],{},[747,15474,15476],{"id":15475},"comparison-with-alternatives","Comparison with Alternatives",[1906,15478,15479,15493],{},[1909,15480,15481],{},[1912,15482,15483,15485,15488,15491],{},[1915,15484,13233],{},[1915,15486,15487],{},"Cost",[1915,15489,15490],{},"Privacy",[1915,15492,13849],{},[1922,15494,15495,15508,15522,15534],{},[1912,15496,15497,15500,15503,15506],{},[1927,15498,15499],{},"Whisper (local)",[1927,15501,15502],{},"Free",[1927,15504,15505],{},"Full privacy",[1927,15507,13917],{},[1912,15509,15510,15513,15516,15519],{},[1927,15511,15512],{},"OpenAI API",[1927,15514,15515],{},"$0.006/min",[1927,15517,15518],{},"Cloud-based",[1927,15520,15521],{},"Excellent",[1912,15523,15524,15527,15530,15532],{},[1927,15525,15526],{},"Google Speech-to-Text",[1927,15528,15529],{},"$0.016/min",[1927,15531,15518],{},[1927,15533,13917],{},[1912,15535,15536,15539,15542,15544],{},[1927,15537,15538],{},"AssemblyAI",[1927,15540,15541],{},"$0.015/min",[1927,15543,15518],{},[1927,15545,13917],{},[752,15547,15548],{},"Choose local Whisper when:",[779,15550,15551,15554,15557],{},[782,15552,15553],{},"Data privacy matters",[782,15555,15556],{},"You have lots of content to transcribe",[782,15558,15559],{},"You want to avoid recurring costs",[767,15561],{},[747,15563,1901],{"id":1900},[752,15565,15566],{},"The video → mp3 → text pipeline with Whisper is straightforward:",[4041,15568,15569,15577],{},[782,15570,15571,13423,15574],{},[755,15572,15573],{},"Extract audio",[920,15575,15576],{},"ffmpeg -i video.mp4 -vn audio.mp3",[782,15578,15579,13423,15582],{},[755,15580,15581],{},"Transcribe",[920,15583,15584],{},"whisper audio.mp3 --language English --model medium",[752,15586,15587],{},"Everything runs locally, it's free, and delivers production-quality results.",[767,15589],{},[747,15591,4324],{"id":4323},[779,15593,15594,15601,15608],{},[782,15595,15596],{},[2036,15597,15600],{"href":15598,"rel":15599},"https://github.com/openai/whisper",[2040],"OpenAI Whisper GitHub",[782,15602,15603],{},[2036,15604,15607],{"href":15605,"rel":15606},"https://ffmpeg.org/documentation.html",[2040],"FFmpeg Documentation",[782,15609,15610],{},[2036,15611,15614],{"href":15612,"rel":15613},"https://github.com/openai/whisper/blob/main/model-card.md",[2040],"Whisper Model Card",[2043,15616,15617],{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}",{"title":934,"searchDepth":967,"depth":967,"links":15619},[15620,15621,15622,15627,15628,15629,15634,15639,15640,15641,15642,15647,15648,15649],{"id":749,"depth":967,"text":750},{"id":13529,"depth":967,"text":13530},{"id":13546,"depth":967,"text":13547,"children":15623},[15624,15625,15626],{"id":13553,"depth":1053,"text":13554},{"id":13573,"depth":1053,"text":13574},{"id":13603,"depth":1053,"text":13604},{"id":13634,"depth":967,"text":13635},{"id":13690,"depth":967,"text":13691},{"id":13798,"depth":967,"text":13799,"children":15630},[15631,15632,15633],{"id":13802,"depth":1053,"text":13803},{"id":13831,"depth":1053,"text":13832},{"id":13948,"depth":1053,"text":13949},{"id":13997,"depth":967,"text":13998,"children":15635},[15636,15637,15638],{"id":14004,"depth":1053,"text":14005},{"id":14035,"depth":1053,"text":14036},{"id":14063,"depth":1053,"text":14064},{"id":14082,"depth":967,"text":14083},{"id":14895,"depth":967,"text":14896},{"id":15125,"depth":967,"text":15126},{"id":15270,"depth":967,"text":15271,"children":15643},[15644,15645,15646],{"id":15274,"depth":1053,"text":15275},{"id":15352,"depth":1053,"text":15353},{"id":15393,"depth":1053,"text":15394},{"id":15475,"depth":967,"text":15476},{"id":1900,"depth":967,"text":1901},{"id":4323,"depth":967,"text":4324},"2026-02-03T00:00:00.000Z","A practical guide to the video → mp3 → text pipeline using OpenAI Whisper. Free, local transcription for interviews, podcasts, and meetings.",{"src":15653},"/images/blog/musictechlab_blog_how-to-transcribe-video-to-text-using-whisper.webp",{"enabled":1056,"items":15655},[15656,15658,15660,15663],{"text":15657,"icon":13495},"Whisper runs locally and for free, with no data sent to the cloud.",{"text":15659,"icon":2081},"The medium model offers the best quality-to-speed ratio for most transcription tasks.",{"text":15661,"icon":15662},"FFmpeg extracts audio from video in one command before Whisper processes it.","i-lucide-terminal",{"text":15664,"icon":15665},"Supports 90+ languages with automatic language detection when the source is unknown.","i-lucide-globe",{},{"title":148,"description":15651},[13502,2094],"BuviUKMZPE9TPst9p1kaETZ_AO8QysHB5AZaUwEOroo",1780305282656]