[{"data":1,"prerenderedAt":15195},["ShallowReactive",2],{"navigation":3,"/blog/software-development/important-new-features-in-python-3-8-post":734,"/blog/software-development/important-new-features-in-python-3-8-surround":896,"/blog/software-development/important-new-features-in-python-3-8-related":901},[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":550,"authors":736,"badge":741,"body":742,"category":873,"client":741,"date":874,"description":875,"extension":876,"faq":741,"featured":69,"featuredOrder":741,"hidden":877,"image":878,"keyTakeaways":880,"meta":891,"navigation":877,"path":551,"seo":892,"status":741,"stem":552,"tags":893,"teaser":741,"__hash__":895},"posts/blog/software-development/important-new-features-in-python-3-8.md",[737],{"name":738,"avatar":739},"Paweł Glimos",{"src":740},"/images/people/pawel-glimos.webp",null,{"type":743,"value":744,"toc":864},"minimark",[745,749,752,757,760,766,776,779,783,786,790,799,802,806,809,813,829,833,846,850,858,861],[746,747,748],"p",{},"hidden: true",[746,750,751],{},"The recent Python 3.8 release introduced many interesting new features, optimizations and tweaks. As new Python versions are published, developers must stay up to date with language changes. In this article, we will try to present the most exciting changes.",[753,754,756],"h2",{"id":755},"assignment-operator","Assignment operator",[746,758,759],{},"The assignment operator lets you create value assignments within larger logic expressions.",[746,761,762],{},[763,764,765],"em",{},"Example",[767,768,769],"blockquote",{},[767,770,771],{},[767,772,773],{},[746,774,775],{},"if ( value := 1 + 1 ) > 1: # 2 > 1\nprint(value)\n2",[746,777,778],{},"In this example, we assign 1+1 to a variable (value) and check if it is greater than 1. It is important to note that you can also use an assignment operator inside list comprehension.",[753,780,782],{"id":781},"new-f-strings-specifier","New f-strings specifier",[746,784,785],{},"Python 3.8 introduces a new ‘=’ specifier available to use inside f-string expressions. The specifier will automatically get the object name with the object value paired.",[746,787,788],{},[763,789,765],{},[767,791,792],{},[767,793,794],{},[767,795,796],{},[746,797,798],{},"object='example_value'\nf'{object}'\n'example_value'\nf'object={object}'\n'object=example_value'\nf'{object=}'\n\"object='example_value'\"\nf'{object=!s}'\n'object=example_value'",[746,800,801],{},"This new feature will certainly be helpful in testing and logging. As you can see in the example, it is also possible to use this conversion flag with the new specifier.",[753,803,805],{"id":804},"new-importlibmetadata-module","New importlib.metadata module",[746,807,808],{},"New importlib.metadata module can help you read metadata, such as the version number for packages installed with tools like pip. Let’s check the installed version of Django and its requirements.",[746,810,811],{},[763,812,765],{},[767,814,815],{},[767,816,817],{},[767,818,819,822],{},[746,820,821],{},"from importlib import metadata\nmetadata.version('django')\n'3.0'",[746,823,824,825],{},"metadata.requires('django')\n",[826,827,828],"span",{},"'pytz', 'sqlparse (>=0.2.2)', 'asgiref (~=3.2)', \"argon2-cffi (>=16.1.0) ; extra == 'argon2'\", \"bcrypt ; extra == 'bcrypt'\"",[753,830,832],{"id":831},"removed-features-in-python-38","Removed features in Python 3.8",[834,835,836,840,843],"ul",{},[837,838,839],"li",{},"The macpath module",[837,841,842],{},"isAlive() method of threading.Thread",[837,844,845],{},"platform.popen() method",[753,847,849],{"id":848},"deprecated-features-in-python-38","Deprecated features in Python 3.8",[834,851,852,855],{},[837,853,854],{},"getchildren() and getiterator() methods of ElementTree",[837,856,857],{},"lgettext(), ldgettext(), lngettext() and ldngettext() of gettext module",[746,859,860],{},"To sum up, if your career path includes Python development, it is worth keeping up to date with python releases systematically. Check out our blog for more news.",[746,862,863],{},"‍",{"title":865,"searchDepth":866,"depth":866,"links":867},"",2,[868,869,870,871,872],{"id":755,"depth":866,"text":756},{"id":781,"depth":866,"text":782},{"id":804,"depth":866,"text":805},{"id":831,"depth":866,"text":832},{"id":848,"depth":866,"text":849},"software-development","2019-12-13T00:00:00.000Z","The new release of the Python programming language brought new features that might be very useful while providing python web development services.","md",true,{"src":879},"/images/blog/musictechlab_blog_important-new-features-in-python-3-8.webp",{"enabled":877,"items":881},[882,885,888],{"text":883,"icon":884},"The walrus operator (:=) enables inline assignment inside expressions like if-statements.","i-lucide-code",{"text":886,"icon":887},"New f-string '=' specifier auto-prints variable names with values for easier debugging.","i-lucide-terminal",{"text":889,"icon":890},"importlib.metadata lets you read installed package versions and dependencies at runtime.","i-lucide-package",{},{"title":550,"description":875},[894],"development","3jefKp8jzpo0FFp4pVCGA0xCOIR-xy443Xeu4E4vfLI",[897,899],{"title":546,"path":547,"stem":548,"description":898,"children":-1},"How we built an automated Notion-to-Markdown sync tool in 3 days, why we left Notion, and why we open-sourced the solution.",{"title":554,"path":555,"stem":556,"description":900,"children":-1},"A step-by-step guide to installing Proxmox on a dedicated OVH server, including network configuration, LXC containers, and backup management tips.",[902,8219,10838,14015],{"id":903,"title":558,"authors":904,"badge":910,"body":913,"category":873,"client":741,"date":8197,"description":8198,"extension":876,"faq":741,"featured":69,"featuredOrder":741,"hidden":69,"image":8199,"keyTakeaways":8202,"meta":8215,"navigation":877,"path":559,"seo":8216,"status":741,"stem":560,"tags":8217,"teaser":741,"__hash__":8218,"score":1060},"posts/blog/software-development/integrating-signnow-e-signatures-into-your-django-application.md",[905],{"name":906,"to":907,"avatar":908},"Mariusz Smenżyk","https://www.linkedin.com/in/mariusz-smenzyk/",{"src":909},"/images/people/mariusz-smenzyk2.webp",{"label":911,"color":912},"API Integration","#6366f1",{"type":743,"value":914,"toc":8173},[915,918,935,938,942,945,1026,1029,1033,1036,1076,1079,1117,1121,1151,1158,1238,1254,1257,1471,1475,1481,1486,1489,2495,2518,2522,2525,3275,3279,3282,3296,3299,3700,3704,3707,4117,4121,4131,4874,4877,4885,4889,4892,6204,6226,6230,6233,6931,6936,6939,7039,7042,7095,7099,7102,7487,7491,7494,7966,7970,7973,7977,7987,7991,7994,7998,8008,8012,8015,8019,8031,8035,8038,8162,8169],[746,916,917],{},"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.",[746,919,920,921,928,929,934],{},"At ",[922,923,927],"a",{"href":924,"rel":925},"https://beatbuddy.pro",[926],"nofollow","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 ",[922,930,933],{"href":931,"rel":932},"https://www.signnow.com/developers",[926],"airSlate SignNow"," for its developer-friendly REST API, generous sandbox environment, and competitive pricing.",[746,936,937],{},"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.",[753,939,941],{"id":940},"why-signnow","Why SignNow?",[746,943,944],{},"Before diving into code, here's why we picked SignNow over alternatives like DocuSign or HelloSign:",[946,947,948,961],"table",{},[949,950,951],"thead",{},[952,953,954,958],"tr",{},[955,956,957],"th",{},"Criteria",[955,959,960],{},"SignNow",[962,963,964,976,986,996,1006,1016],"tbody",{},[952,965,966,973],{},[967,968,969],"td",{},[970,971,972],"strong",{},"Sandbox",[967,974,975],{},"Free, 2,000 signature invites for testing",[952,977,978,983],{},[967,979,980],{},[970,981,982],{},"API style",[967,984,985],{},"Clean REST API with JSON payloads",[952,987,988,993],{},[967,989,990],{},[970,991,992],{},"Authentication",[967,994,995],{},"Standard OAuth2 (password grant)",[952,997,998,1003],{},[967,999,1000],{},[970,1001,1002],{},"Webhooks",[967,1004,1005],{},"Per-document event subscriptions",[952,1007,1008,1013],{},[967,1009,1010],{},[970,1011,1012],{},"Pricing",[967,1014,1015],{},"Significantly cheaper than DocuSign at scale",[952,1017,1018,1023],{},[967,1019,1020],{},[970,1021,1022],{},"SDKs",[967,1024,1025],{},"Official SDKs for Python, Node.js, PHP, Java, C#",[746,1027,1028],{},"For our use case - programmatically sending documents for a single signer - SignNow's API was straightforward and well-documented.",[753,1030,1032],{"id":1031},"architecture-overview","Architecture overview",[746,1034,1035],{},"Here's how the integration fits into our Django application:",[1037,1038,1042],"pre",{"className":1039,"code":1040,"language":1041,"meta":865,"style":865},"language-mermaid shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","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","mermaid",[1043,1044,1045,1053,1058,1064,1070],"code",{"__ignoreMap":865},[826,1046,1049],{"class":1047,"line":1048},"line",1,[826,1050,1052],{"class":1051},"sTEyZ","flowchart TD\n",[826,1054,1055],{"class":1047,"line":866},[826,1056,1057],{"class":1051},"    A[\"Django Admin (trigger sign)\"] --> B[\"Celery Worker (async pipeline)\"]\n",[826,1059,1061],{"class":1047,"line":1060},3,[826,1062,1063],{"class":1051},"    B --> C[\"SignNow API (upload, sign)\"]\n",[826,1065,1067],{"class":1047,"line":1066},4,[826,1068,1069],{"class":1051},"    C -- \"webhook\" --> D[\"Webhook View (POST receiver)\"]\n",[826,1071,1073],{"class":1047,"line":1072},5,[826,1074,1075],{"class":1051},"    D --> E[\"Celery Worker (download PDF)\"]\n",[746,1077,1078],{},"The key design decisions:",[1080,1081,1082,1088,1101,1107],"ol",{},[837,1083,1084,1087],{},[970,1085,1086],{},"Async everything"," - All SignNow API calls happen in Celery tasks, never in the request cycle",[837,1089,1090,1093,1094,1097,1098],{},[970,1091,1092],{},"Generic relations"," - The signing tracker (",[1043,1095,1096],{},"SignableDocument",") can attach to any Django model via ",[1043,1099,1100],{},"ContentType",[837,1102,1103,1106],{},[970,1104,1105],{},"Idempotent operations"," - The pipeline gracefully handles retries and duplicate webhook events",[837,1108,1109,1112,1113,1116],{},[970,1110,1111],{},"Service layer pattern"," - A thin ",[1043,1114,1115],{},"SignNowService"," class wraps all raw API calls",[753,1118,1120],{"id":1119},"step-1-get-your-signnow-api-credentials","Step 1: Get your SignNow API credentials",[1080,1122,1123,1130,1140],{},[837,1124,1125,1126],{},"Create a free sandbox account at ",[922,1127,1129],{"href":931,"rel":1128},[926],"signnow.com/developers",[837,1131,1132,1133,1136,1137],{},"In the API dashboard, create a new application to get your ",[970,1134,1135],{},"Client ID"," and ",[970,1138,1139],{},"Client Secret",[837,1141,1142,1143,1146,1147,1150],{},"Note your sandbox base URL: ",[1043,1144,1145],{},"https://api-eval.signnow.com"," (production uses ",[1043,1148,1149],{},"https://api.signnow.com",")",[746,1152,1153,1154,1157],{},"Add these to your ",[1043,1155,1156],{},".env"," file:",[1037,1159,1163],{"className":1160,"code":1161,"language":1162,"meta":865,"style":865},"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",[1043,1164,1165,1171,1186,1196,1206,1216,1227],{"__ignoreMap":865},[826,1166,1167],{"class":1047,"line":1048},[826,1168,1170],{"class":1169},"sHwdD","# SignNow e-signature (https://www.signnow.com/developers)\n",[826,1172,1173,1176,1180,1183],{"class":1047,"line":866},[826,1174,1175],{"class":1051},"SIGNNOW_API_BASE_URL",[826,1177,1179],{"class":1178},"sMK4o","=",[826,1181,1145],{"class":1182},"sfazB",[826,1184,1185],{"class":1169},"  # Use api.signnow.com for production\n",[826,1187,1188,1191,1193],{"class":1047,"line":1060},[826,1189,1190],{"class":1051},"SIGNNOW_BASIC_AUTH",[826,1192,1179],{"class":1178},[826,1194,1195],{"class":1169},"  # Base64-encoded client_id:client_secret\n",[826,1197,1198,1201,1203],{"class":1047,"line":1066},[826,1199,1200],{"class":1051},"SIGNNOW_USERNAME",[826,1202,1179],{"class":1178},[826,1204,1205],{"class":1169},"    # Your SignNow account email\n",[826,1207,1208,1211,1213],{"class":1047,"line":1072},[826,1209,1210],{"class":1051},"SIGNNOW_PASSWORD",[826,1212,1179],{"class":1178},[826,1214,1215],{"class":1169},"    # Your SignNow account password\n",[826,1217,1219,1222,1224],{"class":1047,"line":1218},6,[826,1220,1221],{"class":1051},"SIGNNOW_WEBHOOK_SECRET",[826,1223,1179],{"class":1178},[826,1225,1226],{"class":1169},"  # For verifying webhook payloads\n",[826,1228,1230,1233,1235],{"class":1047,"line":1229},7,[826,1231,1232],{"class":1051},"SIGNNOW_WEBHOOK_CALLBACK_URL",[826,1234,1179],{"class":1178},[826,1236,1237],{"class":1169},"  # Your public webhook endpoint\n",[1239,1240,1241],"warning",{},[746,1242,1243,1244,1246,1247,1250,1251],{},"The ",[1043,1245,1190],{}," value must be the Base64 encoding of ",[1043,1248,1249],{},"client_id:client_secret",". You can generate it with: ",[1043,1252,1253],{},"echo -n \"your_client_id:your_client_secret\" | base64",[746,1255,1256],{},"Load these in your Django settings:",[1037,1258,1263],{"className":1259,"code":1260,"filename":1261,"language":1262,"meta":865,"style":865},"language-python shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# 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","python",[1043,1264,1265,1270,1315,1347,1378,1409,1440],{"__ignoreMap":865},[826,1266,1267],{"class":1047,"line":1048},[826,1268,1269],{"class":1169},"# SignNow e-signature\n",[826,1271,1272,1275,1277,1280,1283,1287,1290,1293,1295,1297,1300,1304,1306,1308,1310,1312],{"class":1047,"line":866},[826,1273,1274],{"class":1051},"SIGNNOW_API_BASE_URL ",[826,1276,1179],{"class":1178},[826,1278,1279],{"class":1051}," env",[826,1281,1282],{"class":1178},".",[826,1284,1286],{"class":1285},"s2Zo4","str",[826,1288,1289],{"class":1178},"(",[826,1291,1292],{"class":1178},"\"",[826,1294,1175],{"class":1182},[826,1296,1292],{"class":1178},[826,1298,1299],{"class":1178},",",[826,1301,1303],{"class":1302},"sHdIc"," default",[826,1305,1179],{"class":1178},[826,1307,1292],{"class":1178},[826,1309,1149],{"class":1182},[826,1311,1292],{"class":1178},[826,1313,1314],{"class":1178},")\n",[826,1316,1317,1320,1322,1324,1326,1328,1330,1332,1334,1336,1338,1340,1342,1345],{"class":1047,"line":1060},[826,1318,1319],{"class":1051},"SIGNNOW_BASIC_AUTH ",[826,1321,1179],{"class":1178},[826,1323,1279],{"class":1051},[826,1325,1282],{"class":1178},[826,1327,1286],{"class":1285},[826,1329,1289],{"class":1178},[826,1331,1292],{"class":1178},[826,1333,1190],{"class":1182},[826,1335,1292],{"class":1178},[826,1337,1299],{"class":1178},[826,1339,1303],{"class":1302},[826,1341,1179],{"class":1178},[826,1343,1344],{"class":1178},"\"\"",[826,1346,1314],{"class":1178},[826,1348,1349,1352,1354,1356,1358,1360,1362,1364,1366,1368,1370,1372,1374,1376],{"class":1047,"line":1066},[826,1350,1351],{"class":1051},"SIGNNOW_USERNAME ",[826,1353,1179],{"class":1178},[826,1355,1279],{"class":1051},[826,1357,1282],{"class":1178},[826,1359,1286],{"class":1285},[826,1361,1289],{"class":1178},[826,1363,1292],{"class":1178},[826,1365,1200],{"class":1182},[826,1367,1292],{"class":1178},[826,1369,1299],{"class":1178},[826,1371,1303],{"class":1302},[826,1373,1179],{"class":1178},[826,1375,1344],{"class":1178},[826,1377,1314],{"class":1178},[826,1379,1380,1383,1385,1387,1389,1391,1393,1395,1397,1399,1401,1403,1405,1407],{"class":1047,"line":1072},[826,1381,1382],{"class":1051},"SIGNNOW_PASSWORD ",[826,1384,1179],{"class":1178},[826,1386,1279],{"class":1051},[826,1388,1282],{"class":1178},[826,1390,1286],{"class":1285},[826,1392,1289],{"class":1178},[826,1394,1292],{"class":1178},[826,1396,1210],{"class":1182},[826,1398,1292],{"class":1178},[826,1400,1299],{"class":1178},[826,1402,1303],{"class":1302},[826,1404,1179],{"class":1178},[826,1406,1344],{"class":1178},[826,1408,1314],{"class":1178},[826,1410,1411,1414,1416,1418,1420,1422,1424,1426,1428,1430,1432,1434,1436,1438],{"class":1047,"line":1218},[826,1412,1413],{"class":1051},"SIGNNOW_WEBHOOK_SECRET ",[826,1415,1179],{"class":1178},[826,1417,1279],{"class":1051},[826,1419,1282],{"class":1178},[826,1421,1286],{"class":1285},[826,1423,1289],{"class":1178},[826,1425,1292],{"class":1178},[826,1427,1221],{"class":1182},[826,1429,1292],{"class":1178},[826,1431,1299],{"class":1178},[826,1433,1303],{"class":1302},[826,1435,1179],{"class":1178},[826,1437,1344],{"class":1178},[826,1439,1314],{"class":1178},[826,1441,1442,1445,1447,1449,1451,1453,1455,1457,1459,1461,1463,1465,1467,1469],{"class":1047,"line":1229},[826,1443,1444],{"class":1051},"SIGNNOW_WEBHOOK_CALLBACK_URL ",[826,1446,1179],{"class":1178},[826,1448,1279],{"class":1051},[826,1450,1282],{"class":1178},[826,1452,1286],{"class":1285},[826,1454,1289],{"class":1178},[826,1456,1292],{"class":1178},[826,1458,1232],{"class":1182},[826,1460,1292],{"class":1178},[826,1462,1299],{"class":1178},[826,1464,1303],{"class":1302},[826,1466,1179],{"class":1178},[826,1468,1344],{"class":1178},[826,1470,1314],{"class":1178},[753,1472,1474],{"id":1473},"step-2-build-the-api-client-service-layer","Step 2: Build the API client (service layer)",[746,1476,1477,1478,1480],{},"Rather than scattering HTTP calls throughout the codebase, we encapsulated all SignNow API interactions in a single ",[1043,1479,1115],{}," class. This makes testing, error handling, and future changes much simpler.",[1482,1483,1485],"h3",{"id":1484},"oauth2-authentication-with-token-caching","OAuth2 authentication with token caching",[746,1487,1488],{},"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:",[1037,1490,1493],{"className":1259,"code":1491,"filename":1492,"language":1262,"meta":865,"style":865},"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",[1043,1494,1495,1504,1522,1543,1548,1564,1578,1582,1587,1601,1613,1618,1629,1640,1650,1660,1670,1675,1684,1708,1719,1736,1745,1750,1787,1819,1851,1883,1888,1935,1956,1963,1968,1981,1989,1994,2001,2026,2036,2059,2069,2077,2082,2116,2134,2160,2169,2192,2212,2231,2252,2258,2266,2298,2319,2324,2330,2344,2362,2367,2391,2421,2450,2482,2487],{"__ignoreMap":865},[826,1496,1497,1501],{"class":1047,"line":1048},[826,1498,1500],{"class":1499},"s7zQu","import",[826,1502,1503],{"class":1051}," httpx\n",[826,1505,1506,1509,1512,1514,1517,1519],{"class":1047,"line":866},[826,1507,1508],{"class":1499},"from",[826,1510,1511],{"class":1051}," django",[826,1513,1282],{"class":1178},[826,1515,1516],{"class":1051},"conf ",[826,1518,1500],{"class":1499},[826,1520,1521],{"class":1051}," settings\n",[826,1523,1524,1526,1528,1530,1533,1535,1538,1540],{"class":1047,"line":1060},[826,1525,1508],{"class":1499},[826,1527,1511],{"class":1051},[826,1529,1282],{"class":1178},[826,1531,1532],{"class":1051},"core",[826,1534,1282],{"class":1178},[826,1536,1537],{"class":1051},"cache ",[826,1539,1500],{"class":1499},[826,1541,1542],{"class":1051}," cache\n",[826,1544,1545],{"class":1047,"line":1066},[826,1546,1547],{"emptyLinePlaceholder":877},"\n",[826,1549,1550,1553,1555,1558,1561],{"class":1047,"line":1072},[826,1551,1552],{"class":1051},"SIGNNOW_TOKEN_CACHE_KEY ",[826,1554,1179],{"class":1178},[826,1556,1557],{"class":1178}," \"",[826,1559,1560],{"class":1182},"signnow_access_token",[826,1562,1563],{"class":1178},"\"\n",[826,1565,1566,1569,1571,1575],{"class":1047,"line":1218},[826,1567,1568],{"class":1051},"SIGNNOW_TOKEN_TTL_BUFFER ",[826,1570,1179],{"class":1178},[826,1572,1574],{"class":1573},"sbssI"," 300",[826,1576,1577],{"class":1169},"  # Refresh 5 min before expiry\n",[826,1579,1580],{"class":1047,"line":1229},[826,1581,1547],{"emptyLinePlaceholder":877},[826,1583,1585],{"class":1047,"line":1584},8,[826,1586,1547],{"emptyLinePlaceholder":877},[826,1588,1590,1594,1598],{"class":1047,"line":1589},9,[826,1591,1593],{"class":1592},"spNyl","class",[826,1595,1597],{"class":1596},"sBMFI"," SignNowService",[826,1599,1600],{"class":1178},":\n",[826,1602,1604,1607,1610],{"class":1047,"line":1603},10,[826,1605,1606],{"class":1499},"    \"\"\"",[826,1608,1609],{"class":1169},"Raw REST client for the SignNow API using httpx.",[826,1611,1612],{"class":1499},"\"\"\"\n",[826,1614,1616],{"class":1047,"line":1615},11,[826,1617,1547],{"emptyLinePlaceholder":877},[826,1619,1621,1624,1626],{"class":1047,"line":1620},12,[826,1622,1623],{"class":1051},"    _initialized ",[826,1625,1179],{"class":1178},[826,1627,1628],{"class":1178}," False\n",[826,1630,1632,1635,1637],{"class":1047,"line":1631},13,[826,1633,1634],{"class":1051},"    _base_url ",[826,1636,1179],{"class":1178},[826,1638,1639],{"class":1178}," \"\"\n",[826,1641,1643,1646,1648],{"class":1047,"line":1642},14,[826,1644,1645],{"class":1051},"    _basic_auth ",[826,1647,1179],{"class":1178},[826,1649,1639],{"class":1178},[826,1651,1653,1656,1658],{"class":1047,"line":1652},15,[826,1654,1655],{"class":1051},"    _username ",[826,1657,1179],{"class":1178},[826,1659,1639],{"class":1178},[826,1661,1663,1666,1668],{"class":1047,"line":1662},16,[826,1664,1665],{"class":1051},"    _password ",[826,1667,1179],{"class":1178},[826,1669,1639],{"class":1178},[826,1671,1673],{"class":1047,"line":1672},17,[826,1674,1547],{"emptyLinePlaceholder":877},[826,1676,1678,1681],{"class":1047,"line":1677},18,[826,1679,1680],{"class":1178},"    @",[826,1682,1683],{"class":1596},"classmethod\n",[826,1685,1687,1690,1693,1695,1698,1700,1703,1706],{"class":1047,"line":1686},19,[826,1688,1689],{"class":1592},"    def",[826,1691,1692],{"class":1285}," _ensure_initialized",[826,1694,1289],{"class":1178},[826,1696,1697],{"class":1302},"cls",[826,1699,1150],{"class":1178},[826,1701,1702],{"class":1178}," ->",[826,1704,1705],{"class":1596}," bool",[826,1707,1600],{"class":1178},[826,1709,1711,1714,1717],{"class":1047,"line":1710},20,[826,1712,1713],{"class":1499},"        \"\"\"",[826,1715,1716],{"class":1169},"Initialize SignNow configuration from settings.",[826,1718,1612],{"class":1499},[826,1720,1722,1725,1728,1730,1734],{"class":1047,"line":1721},21,[826,1723,1724],{"class":1499},"        if",[826,1726,1727],{"class":1051}," cls",[826,1729,1282],{"class":1178},[826,1731,1733],{"class":1732},"swJcz","_initialized",[826,1735,1600],{"class":1178},[826,1737,1739,1742],{"class":1047,"line":1738},22,[826,1740,1741],{"class":1499},"            return",[826,1743,1744],{"class":1178}," True\n",[826,1746,1748],{"class":1047,"line":1747},23,[826,1749,1547],{"emptyLinePlaceholder":877},[826,1751,1753,1756,1758,1761,1764,1767,1769,1772,1774,1776,1778,1780,1782,1785],{"class":1047,"line":1752},24,[826,1754,1755],{"class":1051},"        cls",[826,1757,1282],{"class":1178},[826,1759,1760],{"class":1732},"_base_url",[826,1762,1763],{"class":1178}," =",[826,1765,1766],{"class":1285}," getattr",[826,1768,1289],{"class":1178},[826,1770,1771],{"class":1285},"settings",[826,1773,1299],{"class":1178},[826,1775,1557],{"class":1178},[826,1777,1175],{"class":1182},[826,1779,1292],{"class":1178},[826,1781,1299],{"class":1178},[826,1783,1784],{"class":1178}," \"\"",[826,1786,1314],{"class":1178},[826,1788,1790,1792,1794,1797,1799,1801,1803,1805,1807,1809,1811,1813,1815,1817],{"class":1047,"line":1789},25,[826,1791,1755],{"class":1051},[826,1793,1282],{"class":1178},[826,1795,1796],{"class":1732},"_basic_auth",[826,1798,1763],{"class":1178},[826,1800,1766],{"class":1285},[826,1802,1289],{"class":1178},[826,1804,1771],{"class":1285},[826,1806,1299],{"class":1178},[826,1808,1557],{"class":1178},[826,1810,1190],{"class":1182},[826,1812,1292],{"class":1178},[826,1814,1299],{"class":1178},[826,1816,1784],{"class":1178},[826,1818,1314],{"class":1178},[826,1820,1822,1824,1826,1829,1831,1833,1835,1837,1839,1841,1843,1845,1847,1849],{"class":1047,"line":1821},26,[826,1823,1755],{"class":1051},[826,1825,1282],{"class":1178},[826,1827,1828],{"class":1732},"_username",[826,1830,1763],{"class":1178},[826,1832,1766],{"class":1285},[826,1834,1289],{"class":1178},[826,1836,1771],{"class":1285},[826,1838,1299],{"class":1178},[826,1840,1557],{"class":1178},[826,1842,1200],{"class":1182},[826,1844,1292],{"class":1178},[826,1846,1299],{"class":1178},[826,1848,1784],{"class":1178},[826,1850,1314],{"class":1178},[826,1852,1854,1856,1858,1861,1863,1865,1867,1869,1871,1873,1875,1877,1879,1881],{"class":1047,"line":1853},27,[826,1855,1755],{"class":1051},[826,1857,1282],{"class":1178},[826,1859,1860],{"class":1732},"_password",[826,1862,1763],{"class":1178},[826,1864,1766],{"class":1285},[826,1866,1289],{"class":1178},[826,1868,1771],{"class":1285},[826,1870,1299],{"class":1178},[826,1872,1557],{"class":1178},[826,1874,1210],{"class":1182},[826,1876,1292],{"class":1178},[826,1878,1299],{"class":1178},[826,1880,1784],{"class":1178},[826,1882,1314],{"class":1178},[826,1884,1886],{"class":1047,"line":1885},28,[826,1887,1547],{"emptyLinePlaceholder":877},[826,1889,1891,1893,1896,1899,1902,1904,1906,1908,1910,1912,1914,1916,1918,1920,1922,1924,1926,1928,1930,1932],{"class":1047,"line":1890},29,[826,1892,1724],{"class":1499},[826,1894,1895],{"class":1178}," not",[826,1897,1898],{"class":1285}," all",[826,1900,1901],{"class":1178},"([",[826,1903,1697],{"class":1051},[826,1905,1282],{"class":1178},[826,1907,1760],{"class":1732},[826,1909,1299],{"class":1178},[826,1911,1727],{"class":1051},[826,1913,1282],{"class":1178},[826,1915,1796],{"class":1732},[826,1917,1299],{"class":1178},[826,1919,1727],{"class":1051},[826,1921,1282],{"class":1178},[826,1923,1828],{"class":1732},[826,1925,1299],{"class":1178},[826,1927,1727],{"class":1051},[826,1929,1282],{"class":1178},[826,1931,1860],{"class":1732},[826,1933,1934],{"class":1178},"]):\n",[826,1936,1938,1941,1943,1945,1947,1949,1952,1954],{"class":1047,"line":1937},30,[826,1939,1940],{"class":1051},"            logger",[826,1942,1282],{"class":1178},[826,1944,1239],{"class":1285},[826,1946,1289],{"class":1178},[826,1948,1292],{"class":1178},[826,1950,1951],{"class":1182},"SignNow API not fully configured - missing required settings",[826,1953,1292],{"class":1178},[826,1955,1314],{"class":1178},[826,1957,1959,1961],{"class":1047,"line":1958},31,[826,1960,1741],{"class":1499},[826,1962,1628],{"class":1178},[826,1964,1966],{"class":1047,"line":1965},32,[826,1967,1547],{"emptyLinePlaceholder":877},[826,1969,1971,1973,1975,1977,1979],{"class":1047,"line":1970},33,[826,1972,1755],{"class":1051},[826,1974,1282],{"class":1178},[826,1976,1733],{"class":1732},[826,1978,1763],{"class":1178},[826,1980,1744],{"class":1178},[826,1982,1984,1987],{"class":1047,"line":1983},34,[826,1985,1986],{"class":1499},"        return",[826,1988,1744],{"class":1178},[826,1990,1992],{"class":1047,"line":1991},35,[826,1993,1547],{"emptyLinePlaceholder":877},[826,1995,1997,1999],{"class":1047,"line":1996},36,[826,1998,1680],{"class":1178},[826,2000,1683],{"class":1596},[826,2002,2004,2006,2009,2011,2013,2015,2017,2020,2023],{"class":1047,"line":2003},37,[826,2005,1689],{"class":1592},[826,2007,2008],{"class":1285}," _get_access_token",[826,2010,1289],{"class":1178},[826,2012,1697],{"class":1302},[826,2014,1150],{"class":1178},[826,2016,1702],{"class":1178},[826,2018,2019],{"class":1596}," str",[826,2021,2022],{"class":1178}," |",[826,2024,2025],{"class":1178}," None:\n",[826,2027,2029,2031,2034],{"class":1047,"line":2028},38,[826,2030,1713],{"class":1499},[826,2032,2033],{"class":1169},"Get a valid access token, using cache or requesting a new one.",[826,2035,1612],{"class":1499},[826,2037,2039,2042,2044,2047,2049,2052,2054,2057],{"class":1047,"line":2038},39,[826,2040,2041],{"class":1051},"        cached ",[826,2043,1179],{"class":1178},[826,2045,2046],{"class":1051}," cache",[826,2048,1282],{"class":1178},[826,2050,2051],{"class":1285},"get",[826,2053,1289],{"class":1178},[826,2055,2056],{"class":1285},"SIGNNOW_TOKEN_CACHE_KEY",[826,2058,1314],{"class":1178},[826,2060,2062,2064,2067],{"class":1047,"line":2061},40,[826,2063,1724],{"class":1499},[826,2065,2066],{"class":1051}," cached",[826,2068,1600],{"class":1178},[826,2070,2072,2074],{"class":1047,"line":2071},41,[826,2073,1741],{"class":1499},[826,2075,2076],{"class":1051}," cached\n",[826,2078,2080],{"class":1047,"line":2079},42,[826,2081,1547],{"emptyLinePlaceholder":877},[826,2083,2085,2088,2091,2093,2096,2098,2101,2103,2106,2108,2111,2114],{"class":1047,"line":2084},43,[826,2086,2087],{"class":1499},"        with",[826,2089,2090],{"class":1051}," httpx",[826,2092,1282],{"class":1178},[826,2094,2095],{"class":1285},"Client",[826,2097,1289],{"class":1178},[826,2099,2100],{"class":1302},"timeout",[826,2102,1179],{"class":1178},[826,2104,2105],{"class":1573},"30.0",[826,2107,1150],{"class":1178},[826,2109,2110],{"class":1499}," as",[826,2112,2113],{"class":1051}," client",[826,2115,1600],{"class":1178},[826,2117,2119,2122,2124,2126,2128,2131],{"class":1047,"line":2118},44,[826,2120,2121],{"class":1051},"            resp ",[826,2123,1179],{"class":1178},[826,2125,2113],{"class":1051},[826,2127,1282],{"class":1178},[826,2129,2130],{"class":1285},"post",[826,2132,2133],{"class":1178},"(\n",[826,2135,2137,2140,2142,2145,2147,2149,2151,2154,2157],{"class":1047,"line":2136},45,[826,2138,2139],{"class":1592},"                f",[826,2141,1292],{"class":1182},[826,2143,2144],{"class":1573},"{",[826,2146,1697],{"class":1051},[826,2148,1282],{"class":1178},[826,2150,1760],{"class":1732},[826,2152,2153],{"class":1573},"}",[826,2155,2156],{"class":1182},"/oauth2/token\"",[826,2158,2159],{"class":1178},",\n",[826,2161,2163,2166],{"class":1047,"line":2162},46,[826,2164,2165],{"class":1302},"                data",[826,2167,2168],{"class":1178},"={\n",[826,2170,2172,2175,2178,2180,2183,2185,2188,2190],{"class":1047,"line":2171},47,[826,2173,2174],{"class":1178},"                    \"",[826,2176,2177],{"class":1182},"grant_type",[826,2179,1292],{"class":1178},[826,2181,2182],{"class":1178},":",[826,2184,1557],{"class":1178},[826,2186,2187],{"class":1182},"password",[826,2189,1292],{"class":1178},[826,2191,2159],{"class":1178},[826,2193,2195,2197,2200,2202,2204,2206,2208,2210],{"class":1047,"line":2194},48,[826,2196,2174],{"class":1178},[826,2198,2199],{"class":1182},"username",[826,2201,1292],{"class":1178},[826,2203,2182],{"class":1178},[826,2205,1727],{"class":1051},[826,2207,1282],{"class":1178},[826,2209,1828],{"class":1732},[826,2211,2159],{"class":1178},[826,2213,2215,2217,2219,2221,2223,2225,2227,2229],{"class":1047,"line":2214},49,[826,2216,2174],{"class":1178},[826,2218,2187],{"class":1182},[826,2220,1292],{"class":1178},[826,2222,2182],{"class":1178},[826,2224,1727],{"class":1051},[826,2226,1282],{"class":1178},[826,2228,1860],{"class":1732},[826,2230,2159],{"class":1178},[826,2232,2234,2236,2239,2241,2243,2245,2248,2250],{"class":1047,"line":2233},50,[826,2235,2174],{"class":1178},[826,2237,2238],{"class":1182},"scope",[826,2240,1292],{"class":1178},[826,2242,2182],{"class":1178},[826,2244,1557],{"class":1178},[826,2246,2247],{"class":1182},"*",[826,2249,1292],{"class":1178},[826,2251,2159],{"class":1178},[826,2253,2255],{"class":1047,"line":2254},51,[826,2256,2257],{"class":1178},"                },\n",[826,2259,2261,2264],{"class":1047,"line":2260},52,[826,2262,2263],{"class":1302},"                headers",[826,2265,2168],{"class":1178},[826,2267,2269,2271,2274,2276,2278,2281,2284,2286,2288,2290,2292,2294,2296],{"class":1047,"line":2268},53,[826,2270,2174],{"class":1178},[826,2272,2273],{"class":1182},"Authorization",[826,2275,1292],{"class":1178},[826,2277,2182],{"class":1178},[826,2279,2280],{"class":1592}," f",[826,2282,2283],{"class":1182},"\"Basic ",[826,2285,2144],{"class":1573},[826,2287,1697],{"class":1051},[826,2289,1282],{"class":1178},[826,2291,1796],{"class":1732},[826,2293,2153],{"class":1573},[826,2295,1292],{"class":1182},[826,2297,2159],{"class":1178},[826,2299,2301,2303,2306,2308,2310,2312,2315,2317],{"class":1047,"line":2300},54,[826,2302,2174],{"class":1178},[826,2304,2305],{"class":1182},"Content-Type",[826,2307,1292],{"class":1178},[826,2309,2182],{"class":1178},[826,2311,1557],{"class":1178},[826,2313,2314],{"class":1182},"application/x-www-form-urlencoded",[826,2316,1292],{"class":1178},[826,2318,2159],{"class":1178},[826,2320,2322],{"class":1047,"line":2321},55,[826,2323,2257],{"class":1178},[826,2325,2327],{"class":1047,"line":2326},56,[826,2328,2329],{"class":1178},"            )\n",[826,2331,2333,2336,2338,2341],{"class":1047,"line":2332},57,[826,2334,2335],{"class":1051},"            resp",[826,2337,1282],{"class":1178},[826,2339,2340],{"class":1285},"raise_for_status",[826,2342,2343],{"class":1178},"()\n",[826,2345,2347,2350,2352,2355,2357,2360],{"class":1047,"line":2346},58,[826,2348,2349],{"class":1051},"            data ",[826,2351,1179],{"class":1178},[826,2353,2354],{"class":1051}," resp",[826,2356,1282],{"class":1178},[826,2358,2359],{"class":1285},"json",[826,2361,2343],{"class":1178},[826,2363,2365],{"class":1047,"line":2364},59,[826,2366,1547],{"emptyLinePlaceholder":877},[826,2368,2370,2373,2375,2378,2381,2383,2386,2388],{"class":1047,"line":2369},60,[826,2371,2372],{"class":1051},"            token ",[826,2374,1179],{"class":1178},[826,2376,2377],{"class":1051}," data",[826,2379,2380],{"class":1178},"[",[826,2382,1292],{"class":1178},[826,2384,2385],{"class":1182},"access_token",[826,2387,1292],{"class":1178},[826,2389,2390],{"class":1178},"]\n",[826,2392,2394,2397,2399,2401,2403,2405,2407,2409,2412,2414,2416,2419],{"class":1047,"line":2393},61,[826,2395,2396],{"class":1051},"            expires_in ",[826,2398,1179],{"class":1178},[826,2400,2377],{"class":1051},[826,2402,1282],{"class":1178},[826,2404,2051],{"class":1285},[826,2406,1289],{"class":1178},[826,2408,1292],{"class":1178},[826,2410,2411],{"class":1182},"expires_in",[826,2413,1292],{"class":1178},[826,2415,1299],{"class":1178},[826,2417,2418],{"class":1573}," 3600",[826,2420,1314],{"class":1178},[826,2422,2424,2427,2429,2432,2434,2437,2440,2443,2445,2448],{"class":1047,"line":2423},62,[826,2425,2426],{"class":1051},"            ttl ",[826,2428,1179],{"class":1178},[826,2430,2431],{"class":1285}," max",[826,2433,1289],{"class":1178},[826,2435,2436],{"class":1285},"expires_in ",[826,2438,2439],{"class":1178},"-",[826,2441,2442],{"class":1285}," SIGNNOW_TOKEN_TTL_BUFFER",[826,2444,1299],{"class":1178},[826,2446,2447],{"class":1573}," 60",[826,2449,1314],{"class":1178},[826,2451,2453,2456,2458,2461,2463,2465,2467,2470,2472,2475,2477,2480],{"class":1047,"line":2452},63,[826,2454,2455],{"class":1051},"            cache",[826,2457,1282],{"class":1178},[826,2459,2460],{"class":1285},"set",[826,2462,1289],{"class":1178},[826,2464,2056],{"class":1285},[826,2466,1299],{"class":1178},[826,2468,2469],{"class":1285}," token",[826,2471,1299],{"class":1178},[826,2473,2474],{"class":1302}," timeout",[826,2476,1179],{"class":1178},[826,2478,2479],{"class":1285},"ttl",[826,2481,1314],{"class":1178},[826,2483,2485],{"class":1047,"line":2484},64,[826,2486,1547],{"emptyLinePlaceholder":877},[826,2488,2490,2492],{"class":1047,"line":2489},65,[826,2491,1741],{"class":1499},[826,2493,2494],{"class":1051}," token\n",[2496,2497,2498],"tip",{},[746,2499,2500,2501,2506,2507,2510,2511,2513,2514,2517],{},"We use ",[922,2502,2505],{"href":2503,"rel":2504},"https://www.python-httpx.org/",[926],"httpx"," instead of ",[1043,2508,2509],{},"requests"," for its modern API, built-in timeout support, and async capabilities. If you later need to make concurrent API calls, ",[1043,2512,2505],{}," supports ",[1043,2515,2516],{},"AsyncClient"," out of the box.",[1482,2519,2521],{"id":2520},"document-operations","Document operations",[746,2523,2524],{},"With authentication handled, the core API methods are straightforward:",[1037,2526,2528],{"className":1259,"code":2527,"filename":1492,"language":1262,"meta":865,"style":865},"@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",[1043,2529,2530,2537,2579,2588,2604,2615,2622,2626,2654,2669,2691,2713,2752,2757,2768,2810,2814,2820,2864,2873,2887,2897,2903,2907,2933,2948,2978,3016,3036,3040,3050,3056,3060,3066,3095,3104,3118,3128,3134,3138,3164,3178,3207,3225,3250,3254,3264],{"__ignoreMap":865},[826,2531,2532,2535],{"class":1047,"line":1048},[826,2533,2534],{"class":1178},"@",[826,2536,1683],{"class":1596},[826,2538,2539,2542,2545,2547,2549,2551,2554,2556,2559,2561,2564,2566,2568,2570,2572,2575,2577],{"class":1047,"line":866},[826,2540,2541],{"class":1592},"def",[826,2543,2544],{"class":1285}," upload_document",[826,2546,1289],{"class":1178},[826,2548,1697],{"class":1302},[826,2550,1299],{"class":1178},[826,2552,2553],{"class":1302}," pdf_bytes",[826,2555,2182],{"class":1178},[826,2557,2558],{"class":1596}," bytes",[826,2560,1299],{"class":1178},[826,2562,2563],{"class":1302}," filename",[826,2565,2182],{"class":1178},[826,2567,2019],{"class":1596},[826,2569,1150],{"class":1178},[826,2571,1702],{"class":1178},[826,2573,2574],{"class":1596}," dict",[826,2576,2022],{"class":1178},[826,2578,2025],{"class":1178},[826,2580,2581,2583,2586],{"class":1047,"line":1060},[826,2582,1606],{"class":1499},[826,2584,2585],{"class":1169},"Upload a PDF to SignNow. Returns {\"id\": \"document_id\"} on success.",[826,2587,1612],{"class":1499},[826,2589,2590,2593,2595,2597,2599,2602],{"class":1047,"line":1066},[826,2591,2592],{"class":1051},"    token ",[826,2594,1179],{"class":1178},[826,2596,1727],{"class":1051},[826,2598,1282],{"class":1178},[826,2600,2601],{"class":1285},"_get_access_token",[826,2603,2343],{"class":1178},[826,2605,2606,2609,2611,2613],{"class":1047,"line":1072},[826,2607,2608],{"class":1499},"    if",[826,2610,1895],{"class":1178},[826,2612,2469],{"class":1051},[826,2614,1600],{"class":1178},[826,2616,2617,2619],{"class":1047,"line":1218},[826,2618,1986],{"class":1499},[826,2620,2621],{"class":1178}," None\n",[826,2623,2624],{"class":1047,"line":1229},[826,2625,1547],{"emptyLinePlaceholder":877},[826,2627,2628,2631,2633,2635,2637,2639,2641,2643,2646,2648,2650,2652],{"class":1047,"line":1584},[826,2629,2630],{"class":1499},"    with",[826,2632,2090],{"class":1051},[826,2634,1282],{"class":1178},[826,2636,2095],{"class":1285},[826,2638,1289],{"class":1178},[826,2640,2100],{"class":1302},[826,2642,1179],{"class":1178},[826,2644,2645],{"class":1573},"60.0",[826,2647,1150],{"class":1178},[826,2649,2110],{"class":1499},[826,2651,2113],{"class":1051},[826,2653,1600],{"class":1178},[826,2655,2656,2659,2661,2663,2665,2667],{"class":1047,"line":1589},[826,2657,2658],{"class":1051},"        resp ",[826,2660,1179],{"class":1178},[826,2662,2113],{"class":1051},[826,2664,1282],{"class":1178},[826,2666,2130],{"class":1285},[826,2668,2133],{"class":1178},[826,2670,2671,2674,2676,2678,2680,2682,2684,2686,2689],{"class":1047,"line":1603},[826,2672,2673],{"class":1592},"            f",[826,2675,1292],{"class":1182},[826,2677,2144],{"class":1573},[826,2679,1697],{"class":1051},[826,2681,1282],{"class":1178},[826,2683,1760],{"class":1732},[826,2685,2153],{"class":1573},[826,2687,2688],{"class":1182},"/document\"",[826,2690,2159],{"class":1178},[826,2692,2693,2696,2698,2700,2702,2705,2707,2710],{"class":1047,"line":1615},[826,2694,2695],{"class":1302},"            headers",[826,2697,1179],{"class":1178},[826,2699,1697],{"class":1051},[826,2701,1282],{"class":1178},[826,2703,2704],{"class":1285},"_auth_headers",[826,2706,1289],{"class":1178},[826,2708,2709],{"class":1285},"token",[826,2711,2712],{"class":1178},"),\n",[826,2714,2715,2718,2721,2723,2726,2728,2730,2733,2736,2738,2740,2742,2744,2747,2749],{"class":1047,"line":1620},[826,2716,2717],{"class":1302},"            files",[826,2719,2720],{"class":1178},"={",[826,2722,1292],{"class":1178},[826,2724,2725],{"class":1182},"file",[826,2727,1292],{"class":1178},[826,2729,2182],{"class":1178},[826,2731,2732],{"class":1178}," (",[826,2734,2735],{"class":1285},"filename",[826,2737,1299],{"class":1178},[826,2739,2553],{"class":1285},[826,2741,1299],{"class":1178},[826,2743,1557],{"class":1178},[826,2745,2746],{"class":1182},"application/pdf",[826,2748,1292],{"class":1178},[826,2750,2751],{"class":1178},")},\n",[826,2753,2754],{"class":1047,"line":1631},[826,2755,2756],{"class":1178},"        )\n",[826,2758,2759,2762,2764,2766],{"class":1047,"line":1642},[826,2760,2761],{"class":1051},"        resp",[826,2763,1282],{"class":1178},[826,2765,2340],{"class":1285},[826,2767,2343],{"class":1178},[826,2769,2770,2772,2775,2777,2780,2782,2784,2786,2788,2790,2793,2795,2797,2799,2801,2803,2805,2807],{"class":1047,"line":1652},[826,2771,1986],{"class":1499},[826,2773,2774],{"class":1178}," {",[826,2776,1292],{"class":1178},[826,2778,2779],{"class":1182},"id",[826,2781,1292],{"class":1178},[826,2783,2182],{"class":1178},[826,2785,2354],{"class":1051},[826,2787,1282],{"class":1178},[826,2789,2359],{"class":1285},[826,2791,2792],{"class":1178},"().",[826,2794,2051],{"class":1285},[826,2796,1289],{"class":1178},[826,2798,1292],{"class":1178},[826,2800,2779],{"class":1182},[826,2802,1292],{"class":1178},[826,2804,1299],{"class":1178},[826,2806,1784],{"class":1178},[826,2808,2809],{"class":1178},")}\n",[826,2811,2812],{"class":1047,"line":1662},[826,2813,1547],{"emptyLinePlaceholder":877},[826,2815,2816,2818],{"class":1047,"line":1672},[826,2817,2534],{"class":1178},[826,2819,1683],{"class":1596},[826,2821,2822,2824,2827,2829,2831,2833,2836,2838,2840,2842,2845,2847,2850,2852,2855,2858,2860,2862],{"class":1047,"line":1677},[826,2823,2541],{"class":1592},[826,2825,2826],{"class":1285}," add_signature_fields",[826,2828,1289],{"class":1178},[826,2830,1697],{"class":1302},[826,2832,1299],{"class":1178},[826,2834,2835],{"class":1302}," document_id",[826,2837,2182],{"class":1178},[826,2839,2019],{"class":1596},[826,2841,1299],{"class":1178},[826,2843,2844],{"class":1302}," fields",[826,2846,2182],{"class":1178},[826,2848,2849],{"class":1051}," list",[826,2851,2380],{"class":1178},[826,2853,2854],{"class":1596},"dict",[826,2856,2857],{"class":1178},"])",[826,2859,1702],{"class":1178},[826,2861,1705],{"class":1596},[826,2863,1600],{"class":1178},[826,2865,2866,2868,2871],{"class":1047,"line":1686},[826,2867,1606],{"class":1499},[826,2869,2870],{"class":1169},"Add signature/text fields to an uploaded document.",[826,2872,1612],{"class":1499},[826,2874,2875,2877,2879,2881,2883,2885],{"class":1047,"line":1710},[826,2876,2592],{"class":1051},[826,2878,1179],{"class":1178},[826,2880,1727],{"class":1051},[826,2882,1282],{"class":1178},[826,2884,2601],{"class":1285},[826,2886,2343],{"class":1178},[826,2888,2889,2891,2893,2895],{"class":1047,"line":1721},[826,2890,2608],{"class":1499},[826,2892,1895],{"class":1178},[826,2894,2469],{"class":1051},[826,2896,1600],{"class":1178},[826,2898,2899,2901],{"class":1047,"line":1738},[826,2900,1986],{"class":1499},[826,2902,1628],{"class":1178},[826,2904,2905],{"class":1047,"line":1747},[826,2906,1547],{"emptyLinePlaceholder":877},[826,2908,2909,2911,2913,2915,2917,2919,2921,2923,2925,2927,2929,2931],{"class":1047,"line":1752},[826,2910,2630],{"class":1499},[826,2912,2090],{"class":1051},[826,2914,1282],{"class":1178},[826,2916,2095],{"class":1285},[826,2918,1289],{"class":1178},[826,2920,2100],{"class":1302},[826,2922,1179],{"class":1178},[826,2924,2105],{"class":1573},[826,2926,1150],{"class":1178},[826,2928,2110],{"class":1499},[826,2930,2113],{"class":1051},[826,2932,1600],{"class":1178},[826,2934,2935,2937,2939,2941,2943,2946],{"class":1047,"line":1789},[826,2936,2658],{"class":1051},[826,2938,1179],{"class":1178},[826,2940,2113],{"class":1051},[826,2942,1282],{"class":1178},[826,2944,2945],{"class":1285},"put",[826,2947,2133],{"class":1178},[826,2949,2950,2952,2954,2956,2958,2960,2962,2964,2967,2969,2972,2974,2976],{"class":1047,"line":1821},[826,2951,2673],{"class":1592},[826,2953,1292],{"class":1182},[826,2955,2144],{"class":1573},[826,2957,1697],{"class":1051},[826,2959,1282],{"class":1178},[826,2961,1760],{"class":1732},[826,2963,2153],{"class":1573},[826,2965,2966],{"class":1182},"/document/",[826,2968,2144],{"class":1573},[826,2970,2971],{"class":1285},"document_id",[826,2973,2153],{"class":1573},[826,2975,1292],{"class":1182},[826,2977,2159],{"class":1178},[826,2979,2980,2982,2985,2987,2989,2991,2993,2995,2998,3000,3002,3004,3006,3008,3011,3013],{"class":1047,"line":1853},[826,2981,2695],{"class":1302},[826,2983,2984],{"class":1178},"={**",[826,2986,1697],{"class":1051},[826,2988,1282],{"class":1178},[826,2990,2704],{"class":1285},[826,2992,1289],{"class":1178},[826,2994,2709],{"class":1285},[826,2996,2997],{"class":1178},"),",[826,2999,1557],{"class":1178},[826,3001,2305],{"class":1182},[826,3003,1292],{"class":1178},[826,3005,2182],{"class":1178},[826,3007,1557],{"class":1178},[826,3009,3010],{"class":1182},"application/json",[826,3012,1292],{"class":1178},[826,3014,3015],{"class":1178},"},\n",[826,3017,3018,3021,3023,3025,3028,3030,3032,3034],{"class":1047,"line":1885},[826,3019,3020],{"class":1302},"            json",[826,3022,2720],{"class":1178},[826,3024,1292],{"class":1178},[826,3026,3027],{"class":1182},"fields",[826,3029,1292],{"class":1178},[826,3031,2182],{"class":1178},[826,3033,2844],{"class":1285},[826,3035,3015],{"class":1178},[826,3037,3038],{"class":1047,"line":1890},[826,3039,2756],{"class":1178},[826,3041,3042,3044,3046,3048],{"class":1047,"line":1937},[826,3043,2761],{"class":1051},[826,3045,1282],{"class":1178},[826,3047,2340],{"class":1285},[826,3049,2343],{"class":1178},[826,3051,3052,3054],{"class":1047,"line":1958},[826,3053,1986],{"class":1499},[826,3055,1744],{"class":1178},[826,3057,3058],{"class":1047,"line":1965},[826,3059,1547],{"emptyLinePlaceholder":877},[826,3061,3062,3064],{"class":1047,"line":1970},[826,3063,2534],{"class":1178},[826,3065,1683],{"class":1596},[826,3067,3068,3070,3073,3075,3077,3079,3081,3083,3085,3087,3089,3091,3093],{"class":1047,"line":1983},[826,3069,2541],{"class":1592},[826,3071,3072],{"class":1285}," download_signed_document",[826,3074,1289],{"class":1178},[826,3076,1697],{"class":1302},[826,3078,1299],{"class":1178},[826,3080,2835],{"class":1302},[826,3082,2182],{"class":1178},[826,3084,2019],{"class":1596},[826,3086,1150],{"class":1178},[826,3088,1702],{"class":1178},[826,3090,2558],{"class":1596},[826,3092,2022],{"class":1178},[826,3094,2025],{"class":1178},[826,3096,3097,3099,3102],{"class":1047,"line":1991},[826,3098,1606],{"class":1499},[826,3100,3101],{"class":1169},"Download the signed (collapsed) PDF.",[826,3103,1612],{"class":1499},[826,3105,3106,3108,3110,3112,3114,3116],{"class":1047,"line":1996},[826,3107,2592],{"class":1051},[826,3109,1179],{"class":1178},[826,3111,1727],{"class":1051},[826,3113,1282],{"class":1178},[826,3115,2601],{"class":1285},[826,3117,2343],{"class":1178},[826,3119,3120,3122,3124,3126],{"class":1047,"line":2003},[826,3121,2608],{"class":1499},[826,3123,1895],{"class":1178},[826,3125,2469],{"class":1051},[826,3127,1600],{"class":1178},[826,3129,3130,3132],{"class":1047,"line":2028},[826,3131,1986],{"class":1499},[826,3133,2621],{"class":1178},[826,3135,3136],{"class":1047,"line":2038},[826,3137,1547],{"emptyLinePlaceholder":877},[826,3139,3140,3142,3144,3146,3148,3150,3152,3154,3156,3158,3160,3162],{"class":1047,"line":2061},[826,3141,2630],{"class":1499},[826,3143,2090],{"class":1051},[826,3145,1282],{"class":1178},[826,3147,2095],{"class":1285},[826,3149,1289],{"class":1178},[826,3151,2100],{"class":1302},[826,3153,1179],{"class":1178},[826,3155,2645],{"class":1573},[826,3157,1150],{"class":1178},[826,3159,2110],{"class":1499},[826,3161,2113],{"class":1051},[826,3163,1600],{"class":1178},[826,3165,3166,3168,3170,3172,3174,3176],{"class":1047,"line":2071},[826,3167,2658],{"class":1051},[826,3169,1179],{"class":1178},[826,3171,2113],{"class":1051},[826,3173,1282],{"class":1178},[826,3175,2051],{"class":1285},[826,3177,2133],{"class":1178},[826,3179,3180,3182,3184,3186,3188,3190,3192,3194,3196,3198,3200,3202,3205],{"class":1047,"line":2079},[826,3181,2673],{"class":1592},[826,3183,1292],{"class":1182},[826,3185,2144],{"class":1573},[826,3187,1697],{"class":1051},[826,3189,1282],{"class":1178},[826,3191,1760],{"class":1732},[826,3193,2153],{"class":1573},[826,3195,2966],{"class":1182},[826,3197,2144],{"class":1573},[826,3199,2971],{"class":1285},[826,3201,2153],{"class":1573},[826,3203,3204],{"class":1182},"/download\"",[826,3206,2159],{"class":1178},[826,3208,3209,3211,3213,3215,3217,3219,3221,3223],{"class":1047,"line":2084},[826,3210,2695],{"class":1302},[826,3212,1179],{"class":1178},[826,3214,1697],{"class":1051},[826,3216,1282],{"class":1178},[826,3218,2704],{"class":1285},[826,3220,1289],{"class":1178},[826,3222,2709],{"class":1285},[826,3224,2712],{"class":1178},[826,3226,3227,3230,3232,3234,3237,3239,3241,3243,3246,3248],{"class":1047,"line":2118},[826,3228,3229],{"class":1302},"            params",[826,3231,2720],{"class":1178},[826,3233,1292],{"class":1178},[826,3235,3236],{"class":1182},"type",[826,3238,1292],{"class":1178},[826,3240,2182],{"class":1178},[826,3242,1557],{"class":1178},[826,3244,3245],{"class":1182},"collapsed",[826,3247,1292],{"class":1178},[826,3249,3015],{"class":1178},[826,3251,3252],{"class":1047,"line":2136},[826,3253,2756],{"class":1178},[826,3255,3256,3258,3260,3262],{"class":1047,"line":2162},[826,3257,2761],{"class":1051},[826,3259,1282],{"class":1178},[826,3261,2340],{"class":1285},[826,3263,2343],{"class":1178},[826,3265,3266,3268,3270,3272],{"class":1047,"line":2171},[826,3267,1986],{"class":1499},[826,3269,2354],{"class":1051},[826,3271,1282],{"class":1178},[826,3273,3274],{"class":1732},"content\n",[1482,3276,3278],{"id":3277},"sending-invites","Sending invites",[746,3280,3281],{},"SignNow supports two invite types:",[834,3283,3284,3290],{},[837,3285,3286,3289],{},[970,3287,3288],{},"Freeform invites"," - The signer places their signature wherever they want",[837,3291,3292,3295],{},[970,3293,3294],{},"Role-based invites"," - Signature fields are pre-positioned, and signers fill specific roles",[746,3297,3298],{},"We used role-based invites because we wanted to control exactly where the signature appears on each document:",[1037,3300,3302],{"className":1259,"code":3301,"filename":1492,"language":1262,"meta":865,"style":865},"@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",[1043,3303,3304,3310,3319,3326,3337,3353,3364,3379,3394,3406,3411,3416,3421,3425,3439,3449,3455,3459,3496,3505,3527,3536,3556,3560,3586,3600,3629,3663,3674,3678,3688],{"__ignoreMap":865},[826,3305,3306,3308],{"class":1047,"line":1048},[826,3307,2534],{"class":1178},[826,3309,1683],{"class":1596},[826,3311,3312,3314,3317],{"class":1047,"line":866},[826,3313,2541],{"class":1592},[826,3315,3316],{"class":1285}," send_role_based_invite",[826,3318,2133],{"class":1178},[826,3320,3321,3324],{"class":1047,"line":1060},[826,3322,3323],{"class":1302},"    cls",[826,3325,2159],{"class":1178},[826,3327,3328,3331,3333,3335],{"class":1047,"line":1066},[826,3329,3330],{"class":1302},"    document_id",[826,3332,2182],{"class":1178},[826,3334,2019],{"class":1596},[826,3336,2159],{"class":1178},[826,3338,3339,3342,3344,3346,3348,3350],{"class":1047,"line":1072},[826,3340,3341],{"class":1302},"    signers",[826,3343,2182],{"class":1178},[826,3345,2849],{"class":1051},[826,3347,2380],{"class":1178},[826,3349,2854],{"class":1596},[826,3351,3352],{"class":1178},"],\n",[826,3354,3355,3358,3360,3362],{"class":1047,"line":1218},[826,3356,3357],{"class":1302},"    from_email",[826,3359,2182],{"class":1178},[826,3361,2019],{"class":1596},[826,3363,2159],{"class":1178},[826,3365,3366,3369,3371,3373,3375,3377],{"class":1047,"line":1229},[826,3367,3368],{"class":1302},"    subject",[826,3370,2182],{"class":1178},[826,3372,2019],{"class":1596},[826,3374,1763],{"class":1178},[826,3376,1784],{"class":1178},[826,3378,2159],{"class":1178},[826,3380,3381,3384,3386,3388,3390,3392],{"class":1047,"line":1584},[826,3382,3383],{"class":1302},"    message",[826,3385,2182],{"class":1178},[826,3387,2019],{"class":1596},[826,3389,1763],{"class":1178},[826,3391,1784],{"class":1178},[826,3393,2159],{"class":1178},[826,3395,3396,3398,3400,3402,3404],{"class":1047,"line":1589},[826,3397,1150],{"class":1178},[826,3399,1702],{"class":1178},[826,3401,2574],{"class":1596},[826,3403,2022],{"class":1178},[826,3405,2025],{"class":1178},[826,3407,3408],{"class":1047,"line":1603},[826,3409,3410],{"class":1499},"    \"\"\"\n",[826,3412,3413],{"class":1047,"line":1615},[826,3414,3415],{"class":1169},"    Send a role-based invite with specific field assignments.\n",[826,3417,3418],{"class":1047,"line":1620},[826,3419,3420],{"class":1169},"    Each signer dict: {\"email\": ..., \"role\": ..., \"role_id\": ..., \"order\": ...}\n",[826,3422,3423],{"class":1047,"line":1631},[826,3424,3410],{"class":1499},[826,3426,3427,3429,3431,3433,3435,3437],{"class":1047,"line":1642},[826,3428,2592],{"class":1051},[826,3430,1179],{"class":1178},[826,3432,1727],{"class":1051},[826,3434,1282],{"class":1178},[826,3436,2601],{"class":1285},[826,3438,2343],{"class":1178},[826,3440,3441,3443,3445,3447],{"class":1047,"line":1652},[826,3442,2608],{"class":1499},[826,3444,1895],{"class":1178},[826,3446,2469],{"class":1051},[826,3448,1600],{"class":1178},[826,3450,3451,3453],{"class":1047,"line":1662},[826,3452,1986],{"class":1499},[826,3454,2621],{"class":1178},[826,3456,3457],{"class":1047,"line":1672},[826,3458,1547],{"emptyLinePlaceholder":877},[826,3460,3461,3464,3466,3468,3470,3473,3475,3477,3480,3482,3484,3486,3488,3490,3493],{"class":1047,"line":1677},[826,3462,3463],{"class":1051},"    payload ",[826,3465,1179],{"class":1178},[826,3467,2774],{"class":1178},[826,3469,1292],{"class":1178},[826,3471,3472],{"class":1182},"to",[826,3474,1292],{"class":1178},[826,3476,2182],{"class":1178},[826,3478,3479],{"class":1051}," signers",[826,3481,1299],{"class":1178},[826,3483,1557],{"class":1178},[826,3485,1508],{"class":1182},[826,3487,1292],{"class":1178},[826,3489,2182],{"class":1178},[826,3491,3492],{"class":1051}," from_email",[826,3494,3495],{"class":1178},"}\n",[826,3497,3498,3500,3503],{"class":1047,"line":1686},[826,3499,2608],{"class":1499},[826,3501,3502],{"class":1051}," subject",[826,3504,1600],{"class":1178},[826,3506,3507,3510,3512,3514,3517,3519,3522,3524],{"class":1047,"line":1710},[826,3508,3509],{"class":1051},"        payload",[826,3511,2380],{"class":1178},[826,3513,1292],{"class":1178},[826,3515,3516],{"class":1182},"subject",[826,3518,1292],{"class":1178},[826,3520,3521],{"class":1178},"]",[826,3523,1763],{"class":1178},[826,3525,3526],{"class":1051}," subject\n",[826,3528,3529,3531,3534],{"class":1047,"line":1721},[826,3530,2608],{"class":1499},[826,3532,3533],{"class":1051}," message",[826,3535,1600],{"class":1178},[826,3537,3538,3540,3542,3544,3547,3549,3551,3553],{"class":1047,"line":1738},[826,3539,3509],{"class":1051},[826,3541,2380],{"class":1178},[826,3543,1292],{"class":1178},[826,3545,3546],{"class":1182},"message",[826,3548,1292],{"class":1178},[826,3550,3521],{"class":1178},[826,3552,1763],{"class":1178},[826,3554,3555],{"class":1051}," message\n",[826,3557,3558],{"class":1047,"line":1747},[826,3559,1547],{"emptyLinePlaceholder":877},[826,3561,3562,3564,3566,3568,3570,3572,3574,3576,3578,3580,3582,3584],{"class":1047,"line":1752},[826,3563,2630],{"class":1499},[826,3565,2090],{"class":1051},[826,3567,1282],{"class":1178},[826,3569,2095],{"class":1285},[826,3571,1289],{"class":1178},[826,3573,2100],{"class":1302},[826,3575,1179],{"class":1178},[826,3577,2105],{"class":1573},[826,3579,1150],{"class":1178},[826,3581,2110],{"class":1499},[826,3583,2113],{"class":1051},[826,3585,1600],{"class":1178},[826,3587,3588,3590,3592,3594,3596,3598],{"class":1047,"line":1789},[826,3589,2658],{"class":1051},[826,3591,1179],{"class":1178},[826,3593,2113],{"class":1051},[826,3595,1282],{"class":1178},[826,3597,2130],{"class":1285},[826,3599,2133],{"class":1178},[826,3601,3602,3604,3606,3608,3610,3612,3614,3616,3618,3620,3622,3624,3627],{"class":1047,"line":1821},[826,3603,2673],{"class":1592},[826,3605,1292],{"class":1182},[826,3607,2144],{"class":1573},[826,3609,1697],{"class":1051},[826,3611,1282],{"class":1178},[826,3613,1760],{"class":1732},[826,3615,2153],{"class":1573},[826,3617,2966],{"class":1182},[826,3619,2144],{"class":1573},[826,3621,2971],{"class":1285},[826,3623,2153],{"class":1573},[826,3625,3626],{"class":1182},"/invite\"",[826,3628,2159],{"class":1178},[826,3630,3631,3633,3635,3637,3639,3641,3643,3645,3647,3649,3651,3653,3655,3657,3659,3661],{"class":1047,"line":1853},[826,3632,2695],{"class":1302},[826,3634,2984],{"class":1178},[826,3636,1697],{"class":1051},[826,3638,1282],{"class":1178},[826,3640,2704],{"class":1285},[826,3642,1289],{"class":1178},[826,3644,2709],{"class":1285},[826,3646,2997],{"class":1178},[826,3648,1557],{"class":1178},[826,3650,2305],{"class":1182},[826,3652,1292],{"class":1178},[826,3654,2182],{"class":1178},[826,3656,1557],{"class":1178},[826,3658,3010],{"class":1182},[826,3660,1292],{"class":1178},[826,3662,3015],{"class":1178},[826,3664,3665,3667,3669,3672],{"class":1047,"line":1885},[826,3666,3020],{"class":1302},[826,3668,1179],{"class":1178},[826,3670,3671],{"class":1285},"payload",[826,3673,2159],{"class":1178},[826,3675,3676],{"class":1047,"line":1890},[826,3677,2756],{"class":1178},[826,3679,3680,3682,3684,3686],{"class":1047,"line":1937},[826,3681,2761],{"class":1051},[826,3683,1282],{"class":1178},[826,3685,2340],{"class":1285},[826,3687,2343],{"class":1178},[826,3689,3690,3692,3694,3696,3698],{"class":1047,"line":1958},[826,3691,1986],{"class":1499},[826,3693,2354],{"class":1051},[826,3695,1282],{"class":1178},[826,3697,2359],{"class":1285},[826,3699,2343],{"class":1178},[1482,3701,3703],{"id":3702},"webhook-registration","Webhook registration",[746,3705,3706],{},"To get notified when a document is signed, we register a webhook for each document:",[1037,3708,3710],{"className":1259,"code":3709,"filename":1492,"language":1262,"meta":865,"style":865},"@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",[1043,3711,3712,3718,3764,3773,3787,3797,3803,3807,3816,3832,3847,3867,3880,3895,3909,3914,3919,3923,3950,3959,3988,3992,4018,4032,4053,4087,4097,4101,4111],{"__ignoreMap":865},[826,3713,3714,3716],{"class":1047,"line":1048},[826,3715,2534],{"class":1178},[826,3717,1683],{"class":1596},[826,3719,3720,3722,3725,3727,3729,3731,3734,3736,3738,3740,3743,3745,3747,3749,3752,3754,3756,3758,3760,3762],{"class":1047,"line":866},[826,3721,2541],{"class":1592},[826,3723,3724],{"class":1285}," register_webhook",[826,3726,1289],{"class":1178},[826,3728,1697],{"class":1302},[826,3730,1299],{"class":1178},[826,3732,3733],{"class":1302}," event",[826,3735,2182],{"class":1178},[826,3737,2019],{"class":1596},[826,3739,1299],{"class":1178},[826,3741,3742],{"class":1302}," entity_id",[826,3744,2182],{"class":1178},[826,3746,2019],{"class":1596},[826,3748,1299],{"class":1178},[826,3750,3751],{"class":1302}," callback_url",[826,3753,2182],{"class":1178},[826,3755,2019],{"class":1596},[826,3757,1150],{"class":1178},[826,3759,1702],{"class":1178},[826,3761,1705],{"class":1596},[826,3763,1600],{"class":1178},[826,3765,3766,3768,3771],{"class":1047,"line":1060},[826,3767,1606],{"class":1499},[826,3769,3770],{"class":1169},"Register a webhook callback for a specific event on a document.",[826,3772,1612],{"class":1499},[826,3774,3775,3777,3779,3781,3783,3785],{"class":1047,"line":1066},[826,3776,2592],{"class":1051},[826,3778,1179],{"class":1178},[826,3780,1727],{"class":1051},[826,3782,1282],{"class":1178},[826,3784,2601],{"class":1285},[826,3786,2343],{"class":1178},[826,3788,3789,3791,3793,3795],{"class":1047,"line":1072},[826,3790,2608],{"class":1499},[826,3792,1895],{"class":1178},[826,3794,2469],{"class":1051},[826,3796,1600],{"class":1178},[826,3798,3799,3801],{"class":1047,"line":1218},[826,3800,1986],{"class":1499},[826,3802,1628],{"class":1178},[826,3804,3805],{"class":1047,"line":1229},[826,3806,1547],{"emptyLinePlaceholder":877},[826,3808,3809,3811,3813],{"class":1047,"line":1584},[826,3810,3463],{"class":1051},[826,3812,1179],{"class":1178},[826,3814,3815],{"class":1178}," {\n",[826,3817,3818,3821,3824,3826,3828,3830],{"class":1047,"line":1589},[826,3819,3820],{"class":1178},"        \"",[826,3822,3823],{"class":1182},"event",[826,3825,1292],{"class":1178},[826,3827,2182],{"class":1178},[826,3829,3733],{"class":1051},[826,3831,2159],{"class":1178},[826,3833,3834,3836,3839,3841,3843,3845],{"class":1047,"line":1603},[826,3835,3820],{"class":1178},[826,3837,3838],{"class":1182},"entity_id",[826,3840,1292],{"class":1178},[826,3842,2182],{"class":1178},[826,3844,3742],{"class":1051},[826,3846,2159],{"class":1178},[826,3848,3849,3851,3854,3856,3858,3860,3863,3865],{"class":1047,"line":1615},[826,3850,3820],{"class":1178},[826,3852,3853],{"class":1182},"action",[826,3855,1292],{"class":1178},[826,3857,2182],{"class":1178},[826,3859,1557],{"class":1178},[826,3861,3862],{"class":1182},"callback",[826,3864,1292],{"class":1178},[826,3866,2159],{"class":1178},[826,3868,3869,3871,3874,3876,3878],{"class":1047,"line":1620},[826,3870,3820],{"class":1178},[826,3872,3873],{"class":1182},"attributes",[826,3875,1292],{"class":1178},[826,3877,2182],{"class":1178},[826,3879,3815],{"class":1178},[826,3881,3882,3885,3887,3889,3891,3893],{"class":1047,"line":1631},[826,3883,3884],{"class":1178},"            \"",[826,3886,3862],{"class":1182},[826,3888,1292],{"class":1178},[826,3890,2182],{"class":1178},[826,3892,3751],{"class":1051},[826,3894,2159],{"class":1178},[826,3896,3897,3899,3902,3904,3906],{"class":1047,"line":1642},[826,3898,3884],{"class":1178},[826,3900,3901],{"class":1182},"use_tls_12",[826,3903,1292],{"class":1178},[826,3905,2182],{"class":1178},[826,3907,3908],{"class":1178}," True,\n",[826,3910,3911],{"class":1047,"line":1652},[826,3912,3913],{"class":1178},"        },\n",[826,3915,3916],{"class":1047,"line":1662},[826,3917,3918],{"class":1178},"    }\n",[826,3920,3921],{"class":1047,"line":1672},[826,3922,1547],{"emptyLinePlaceholder":877},[826,3924,3925,3928,3930,3932,3934,3936,3938,3940,3942,3944,3946,3948],{"class":1047,"line":1677},[826,3926,3927],{"class":1051},"    webhook_secret ",[826,3929,1179],{"class":1178},[826,3931,1766],{"class":1285},[826,3933,1289],{"class":1178},[826,3935,1771],{"class":1285},[826,3937,1299],{"class":1178},[826,3939,1557],{"class":1178},[826,3941,1221],{"class":1182},[826,3943,1292],{"class":1178},[826,3945,1299],{"class":1178},[826,3947,1784],{"class":1178},[826,3949,1314],{"class":1178},[826,3951,3952,3954,3957],{"class":1047,"line":1686},[826,3953,2608],{"class":1499},[826,3955,3956],{"class":1051}," webhook_secret",[826,3958,1600],{"class":1178},[826,3960,3961,3963,3965,3967,3969,3971,3974,3976,3979,3981,3983,3985],{"class":1047,"line":1710},[826,3962,3509],{"class":1051},[826,3964,2380],{"class":1178},[826,3966,1292],{"class":1178},[826,3968,3873],{"class":1182},[826,3970,1292],{"class":1178},[826,3972,3973],{"class":1178},"][",[826,3975,1292],{"class":1178},[826,3977,3978],{"class":1182},"secret_key",[826,3980,1292],{"class":1178},[826,3982,3521],{"class":1178},[826,3984,1763],{"class":1178},[826,3986,3987],{"class":1051}," webhook_secret\n",[826,3989,3990],{"class":1047,"line":1721},[826,3991,1547],{"emptyLinePlaceholder":877},[826,3993,3994,3996,3998,4000,4002,4004,4006,4008,4010,4012,4014,4016],{"class":1047,"line":1738},[826,3995,2630],{"class":1499},[826,3997,2090],{"class":1051},[826,3999,1282],{"class":1178},[826,4001,2095],{"class":1285},[826,4003,1289],{"class":1178},[826,4005,2100],{"class":1302},[826,4007,1179],{"class":1178},[826,4009,2105],{"class":1573},[826,4011,1150],{"class":1178},[826,4013,2110],{"class":1499},[826,4015,2113],{"class":1051},[826,4017,1600],{"class":1178},[826,4019,4020,4022,4024,4026,4028,4030],{"class":1047,"line":1747},[826,4021,2658],{"class":1051},[826,4023,1179],{"class":1178},[826,4025,2113],{"class":1051},[826,4027,1282],{"class":1178},[826,4029,2130],{"class":1285},[826,4031,2133],{"class":1178},[826,4033,4034,4036,4038,4040,4042,4044,4046,4048,4051],{"class":1047,"line":1752},[826,4035,2673],{"class":1592},[826,4037,1292],{"class":1182},[826,4039,2144],{"class":1573},[826,4041,1697],{"class":1051},[826,4043,1282],{"class":1178},[826,4045,1760],{"class":1732},[826,4047,2153],{"class":1573},[826,4049,4050],{"class":1182},"/v2/events\"",[826,4052,2159],{"class":1178},[826,4054,4055,4057,4059,4061,4063,4065,4067,4069,4071,4073,4075,4077,4079,4081,4083,4085],{"class":1047,"line":1789},[826,4056,2695],{"class":1302},[826,4058,2984],{"class":1178},[826,4060,1697],{"class":1051},[826,4062,1282],{"class":1178},[826,4064,2704],{"class":1285},[826,4066,1289],{"class":1178},[826,4068,2709],{"class":1285},[826,4070,2997],{"class":1178},[826,4072,1557],{"class":1178},[826,4074,2305],{"class":1182},[826,4076,1292],{"class":1178},[826,4078,2182],{"class":1178},[826,4080,1557],{"class":1178},[826,4082,3010],{"class":1182},[826,4084,1292],{"class":1178},[826,4086,3015],{"class":1178},[826,4088,4089,4091,4093,4095],{"class":1047,"line":1821},[826,4090,3020],{"class":1302},[826,4092,1179],{"class":1178},[826,4094,3671],{"class":1285},[826,4096,2159],{"class":1178},[826,4098,4099],{"class":1047,"line":1853},[826,4100,2756],{"class":1178},[826,4102,4103,4105,4107,4109],{"class":1047,"line":1885},[826,4104,2761],{"class":1051},[826,4106,1282],{"class":1178},[826,4108,2340],{"class":1285},[826,4110,2343],{"class":1178},[826,4112,4113,4115],{"class":1047,"line":1890},[826,4114,1986],{"class":1499},[826,4116,1744],{"class":1178},[753,4118,4120],{"id":4119},"step-3-track-document-state-with-a-django-model","Step 3: Track document state with a Django model",[746,4122,4123,4124,4126,4127,4130],{},"We need to track each document's journey through the signing pipeline. The ",[1043,4125,1096],{}," model uses Django's ",[1043,4128,4129],{},"GenericForeignKey"," so it can be attached to any model in the system - a deposit confirmation, an NDA, a beta testing agreement, etc.",[1037,4132,4135],{"className":1259,"code":4133,"filename":4134,"language":1262,"meta":865,"style":865},"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",[1043,4136,4137,4163,4187,4203,4207,4211,4231,4240,4244,4263,4286,4309,4332,4355,4378,4401,4405,4410,4445,4461,4491,4495,4500,4538,4565,4569,4574,4590,4618,4622,4627,4679,4700,4704,4709,4737,4771,4775,4780,4805,4828,4851],{"__ignoreMap":865},[826,4138,4139,4141,4143,4145,4148,4150,4153,4155,4158,4160],{"class":1047,"line":1048},[826,4140,1508],{"class":1499},[826,4142,1511],{"class":1051},[826,4144,1282],{"class":1178},[826,4146,4147],{"class":1051},"contrib",[826,4149,1282],{"class":1178},[826,4151,4152],{"class":1051},"contenttypes",[826,4154,1282],{"class":1178},[826,4156,4157],{"class":1051},"fields ",[826,4159,1500],{"class":1499},[826,4161,4162],{"class":1051}," GenericForeignKey\n",[826,4164,4165,4167,4169,4171,4173,4175,4177,4179,4182,4184],{"class":1047,"line":866},[826,4166,1508],{"class":1499},[826,4168,1511],{"class":1051},[826,4170,1282],{"class":1178},[826,4172,4147],{"class":1051},[826,4174,1282],{"class":1178},[826,4176,4152],{"class":1051},[826,4178,1282],{"class":1178},[826,4180,4181],{"class":1051},"models ",[826,4183,1500],{"class":1499},[826,4185,4186],{"class":1051}," ContentType\n",[826,4188,4189,4191,4193,4195,4198,4200],{"class":1047,"line":1060},[826,4190,1508],{"class":1499},[826,4192,1511],{"class":1051},[826,4194,1282],{"class":1178},[826,4196,4197],{"class":1051},"db ",[826,4199,1500],{"class":1499},[826,4201,4202],{"class":1051}," models\n",[826,4204,4205],{"class":1047,"line":1066},[826,4206,1547],{"emptyLinePlaceholder":877},[826,4208,4209],{"class":1047,"line":1072},[826,4210,1547],{"emptyLinePlaceholder":877},[826,4212,4213,4215,4218,4220,4223,4225,4228],{"class":1047,"line":1218},[826,4214,1593],{"class":1592},[826,4216,4217],{"class":1596}," SignableDocument",[826,4219,1289],{"class":1178},[826,4221,4222],{"class":1596},"models",[826,4224,1282],{"class":1178},[826,4226,4227],{"class":1596},"Model",[826,4229,4230],{"class":1178},"):\n",[826,4232,4233,4235,4238],{"class":1047,"line":1229},[826,4234,1606],{"class":1499},[826,4236,4237],{"class":1169},"Tracks a document uploaded to SignNow for e-signature.",[826,4239,1612],{"class":1499},[826,4241,4242],{"class":1047,"line":1584},[826,4243,1547],{"emptyLinePlaceholder":877},[826,4245,4246,4249,4252,4254,4256,4258,4261],{"class":1047,"line":1589},[826,4247,4248],{"class":1592},"    class",[826,4250,4251],{"class":1596}," Status",[826,4253,1289],{"class":1178},[826,4255,4222],{"class":1596},[826,4257,1282],{"class":1178},[826,4259,4260],{"class":1596},"TextChoices",[826,4262,4230],{"class":1178},[826,4264,4265,4268,4270,4272,4275,4277,4279,4281,4284],{"class":1047,"line":1603},[826,4266,4267],{"class":1051},"        PENDING ",[826,4269,1179],{"class":1178},[826,4271,1557],{"class":1178},[826,4273,4274],{"class":1182},"pending",[826,4276,1292],{"class":1178},[826,4278,1299],{"class":1178},[826,4280,1557],{"class":1178},[826,4282,4283],{"class":1182},"Pending Upload",[826,4285,1563],{"class":1178},[826,4287,4288,4291,4293,4295,4298,4300,4302,4304,4307],{"class":1047,"line":1615},[826,4289,4290],{"class":1051},"        UPLOADED ",[826,4292,1179],{"class":1178},[826,4294,1557],{"class":1178},[826,4296,4297],{"class":1182},"uploaded",[826,4299,1292],{"class":1178},[826,4301,1299],{"class":1178},[826,4303,1557],{"class":1178},[826,4305,4306],{"class":1182},"Uploaded to SignNow",[826,4308,1563],{"class":1178},[826,4310,4311,4314,4316,4318,4321,4323,4325,4327,4330],{"class":1047,"line":1620},[826,4312,4313],{"class":1051},"        INVITE_SENT ",[826,4315,1179],{"class":1178},[826,4317,1557],{"class":1178},[826,4319,4320],{"class":1182},"invite_sent",[826,4322,1292],{"class":1178},[826,4324,1299],{"class":1178},[826,4326,1557],{"class":1178},[826,4328,4329],{"class":1182},"Invite Sent",[826,4331,1563],{"class":1178},[826,4333,4334,4337,4339,4341,4344,4346,4348,4350,4353],{"class":1047,"line":1631},[826,4335,4336],{"class":1051},"        SIGNED ",[826,4338,1179],{"class":1178},[826,4340,1557],{"class":1178},[826,4342,4343],{"class":1182},"signed",[826,4345,1292],{"class":1178},[826,4347,1299],{"class":1178},[826,4349,1557],{"class":1178},[826,4351,4352],{"class":1182},"Signed",[826,4354,1563],{"class":1178},[826,4356,4357,4360,4362,4364,4367,4369,4371,4373,4376],{"class":1047,"line":1642},[826,4358,4359],{"class":1051},"        DOWNLOADED ",[826,4361,1179],{"class":1178},[826,4363,1557],{"class":1178},[826,4365,4366],{"class":1182},"downloaded",[826,4368,1292],{"class":1178},[826,4370,1299],{"class":1178},[826,4372,1557],{"class":1178},[826,4374,4375],{"class":1182},"Signed PDF Downloaded",[826,4377,1563],{"class":1178},[826,4379,4380,4383,4385,4387,4390,4392,4394,4396,4399],{"class":1047,"line":1652},[826,4381,4382],{"class":1051},"        FAILED ",[826,4384,1179],{"class":1178},[826,4386,1557],{"class":1178},[826,4388,4389],{"class":1182},"failed",[826,4391,1292],{"class":1178},[826,4393,1299],{"class":1178},[826,4395,1557],{"class":1178},[826,4397,4398],{"class":1182},"Failed",[826,4400,1563],{"class":1178},[826,4402,4403],{"class":1047,"line":1662},[826,4404,1547],{"emptyLinePlaceholder":877},[826,4406,4407],{"class":1047,"line":1672},[826,4408,4409],{"class":1169},"    # Generic relation - attach to any Django model\n",[826,4411,4412,4415,4417,4420,4422,4425,4427,4429,4431,4434,4436,4438,4440,4443],{"class":1047,"line":1677},[826,4413,4414],{"class":1051},"    content_type ",[826,4416,1179],{"class":1178},[826,4418,4419],{"class":1051}," models",[826,4421,1282],{"class":1178},[826,4423,4424],{"class":1285},"ForeignKey",[826,4426,1289],{"class":1178},[826,4428,1100],{"class":1285},[826,4430,1299],{"class":1178},[826,4432,4433],{"class":1302}," on_delete",[826,4435,1179],{"class":1178},[826,4437,4222],{"class":1285},[826,4439,1282],{"class":1178},[826,4441,4442],{"class":1732},"CASCADE",[826,4444,1314],{"class":1178},[826,4446,4447,4450,4452,4454,4456,4459],{"class":1047,"line":1686},[826,4448,4449],{"class":1051},"    object_id ",[826,4451,1179],{"class":1178},[826,4453,4419],{"class":1051},[826,4455,1282],{"class":1178},[826,4457,4458],{"class":1285},"PositiveIntegerField",[826,4460,2343],{"class":1178},[826,4462,4463,4466,4468,4471,4473,4475,4478,4480,4482,4484,4487,4489],{"class":1047,"line":1710},[826,4464,4465],{"class":1051},"    source_document ",[826,4467,1179],{"class":1178},[826,4469,4470],{"class":1285}," GenericForeignKey",[826,4472,1289],{"class":1178},[826,4474,1292],{"class":1178},[826,4476,4477],{"class":1182},"content_type",[826,4479,1292],{"class":1178},[826,4481,1299],{"class":1178},[826,4483,1557],{"class":1178},[826,4485,4486],{"class":1182},"object_id",[826,4488,1292],{"class":1178},[826,4490,1314],{"class":1178},[826,4492,4493],{"class":1047,"line":1721},[826,4494,1547],{"emptyLinePlaceholder":877},[826,4496,4497],{"class":1047,"line":1738},[826,4498,4499],{"class":1169},"    # SignNow tracking\n",[826,4501,4502,4505,4507,4509,4511,4514,4516,4519,4521,4524,4526,4529,4532,4535],{"class":1047,"line":1747},[826,4503,4504],{"class":1051},"    signnow_document_id ",[826,4506,1179],{"class":1178},[826,4508,4419],{"class":1051},[826,4510,1282],{"class":1178},[826,4512,4513],{"class":1285},"CharField",[826,4515,1289],{"class":1178},[826,4517,4518],{"class":1302},"max_length",[826,4520,1179],{"class":1178},[826,4522,4523],{"class":1573},"64",[826,4525,1299],{"class":1178},[826,4527,4528],{"class":1302}," blank",[826,4530,4531],{"class":1178},"=True,",[826,4533,4534],{"class":1302}," db_index",[826,4536,4537],{"class":1178},"=True)\n",[826,4539,4540,4543,4545,4547,4549,4551,4553,4555,4557,4559,4561,4563],{"class":1047,"line":1752},[826,4541,4542],{"class":1051},"    signnow_invite_id ",[826,4544,1179],{"class":1178},[826,4546,4419],{"class":1051},[826,4548,1282],{"class":1178},[826,4550,4513],{"class":1285},[826,4552,1289],{"class":1178},[826,4554,4518],{"class":1302},[826,4556,1179],{"class":1178},[826,4558,4523],{"class":1573},[826,4560,1299],{"class":1178},[826,4562,4528],{"class":1302},[826,4564,4537],{"class":1178},[826,4566,4567],{"class":1047,"line":1789},[826,4568,1547],{"emptyLinePlaceholder":877},[826,4570,4571],{"class":1047,"line":1821},[826,4572,4573],{"class":1169},"    # Signer info\n",[826,4575,4576,4579,4581,4583,4585,4588],{"class":1047,"line":1853},[826,4577,4578],{"class":1051},"    signer_email ",[826,4580,1179],{"class":1178},[826,4582,4419],{"class":1051},[826,4584,1282],{"class":1178},[826,4586,4587],{"class":1285},"EmailField",[826,4589,2343],{"class":1178},[826,4591,4592,4595,4597,4599,4601,4603,4605,4607,4609,4612,4614,4616],{"class":1047,"line":1885},[826,4593,4594],{"class":1051},"    signer_name ",[826,4596,1179],{"class":1178},[826,4598,4419],{"class":1051},[826,4600,1282],{"class":1178},[826,4602,4513],{"class":1285},[826,4604,1289],{"class":1178},[826,4606,4518],{"class":1302},[826,4608,1179],{"class":1178},[826,4610,4611],{"class":1573},"255",[826,4613,1299],{"class":1178},[826,4615,4528],{"class":1302},[826,4617,4537],{"class":1178},[826,4619,4620],{"class":1047,"line":1890},[826,4621,1547],{"emptyLinePlaceholder":877},[826,4623,4624],{"class":1047,"line":1937},[826,4625,4626],{"class":1169},"    # Status & errors\n",[826,4628,4629,4632,4634,4636,4638,4640,4642,4644,4646,4649,4651,4654,4656,4659,4661,4664,4666,4668,4670,4672,4674,4677],{"class":1047,"line":1958},[826,4630,4631],{"class":1051},"    status ",[826,4633,1179],{"class":1178},[826,4635,4419],{"class":1051},[826,4637,1282],{"class":1178},[826,4639,4513],{"class":1285},[826,4641,1289],{"class":1178},[826,4643,4518],{"class":1302},[826,4645,1179],{"class":1178},[826,4647,4648],{"class":1573},"16",[826,4650,1299],{"class":1178},[826,4652,4653],{"class":1302}," choices",[826,4655,1179],{"class":1178},[826,4657,4658],{"class":1285},"Status",[826,4660,1282],{"class":1178},[826,4662,4663],{"class":1732},"choices",[826,4665,1299],{"class":1178},[826,4667,1303],{"class":1302},[826,4669,1179],{"class":1178},[826,4671,4658],{"class":1285},[826,4673,1282],{"class":1178},[826,4675,4676],{"class":1732},"PENDING",[826,4678,1314],{"class":1178},[826,4680,4681,4684,4686,4688,4690,4693,4695,4698],{"class":1047,"line":1965},[826,4682,4683],{"class":1051},"    error_message ",[826,4685,1179],{"class":1178},[826,4687,4419],{"class":1051},[826,4689,1282],{"class":1178},[826,4691,4692],{"class":1285},"TextField",[826,4694,1289],{"class":1178},[826,4696,4697],{"class":1302},"blank",[826,4699,4537],{"class":1178},[826,4701,4702],{"class":1047,"line":1970},[826,4703,1547],{"emptyLinePlaceholder":877},[826,4705,4706],{"class":1047,"line":1983},[826,4707,4708],{"class":1169},"    # Files\n",[826,4710,4711,4714,4716,4718,4720,4722,4724,4726,4728,4731,4733,4735],{"class":1047,"line":1991},[826,4712,4713],{"class":1051},"    original_pdf_name ",[826,4715,1179],{"class":1178},[826,4717,4419],{"class":1051},[826,4719,1282],{"class":1178},[826,4721,4513],{"class":1285},[826,4723,1289],{"class":1178},[826,4725,4518],{"class":1302},[826,4727,1179],{"class":1178},[826,4729,4730],{"class":1573},"512",[826,4732,1299],{"class":1178},[826,4734,4528],{"class":1302},[826,4736,4537],{"class":1178},[826,4738,4739,4742,4744,4746,4748,4751,4753,4756,4758,4760,4763,4765,4767,4769],{"class":1047,"line":1996},[826,4740,4741],{"class":1051},"    signed_pdf ",[826,4743,1179],{"class":1178},[826,4745,4419],{"class":1051},[826,4747,1282],{"class":1178},[826,4749,4750],{"class":1285},"FileField",[826,4752,1289],{"class":1178},[826,4754,4755],{"class":1302},"upload_to",[826,4757,1179],{"class":1178},[826,4759,1292],{"class":1178},[826,4761,4762],{"class":1182},"signnow/signed/%Y/%m/",[826,4764,1292],{"class":1178},[826,4766,1299],{"class":1178},[826,4768,4528],{"class":1302},[826,4770,4537],{"class":1178},[826,4772,4773],{"class":1047,"line":2003},[826,4774,1547],{"emptyLinePlaceholder":877},[826,4776,4777],{"class":1047,"line":2028},[826,4778,4779],{"class":1169},"    # Timestamps\n",[826,4781,4782,4785,4787,4789,4791,4794,4796,4799,4801,4803],{"class":1047,"line":2038},[826,4783,4784],{"class":1051},"    uploaded_at ",[826,4786,1179],{"class":1178},[826,4788,4419],{"class":1051},[826,4790,1282],{"class":1178},[826,4792,4793],{"class":1285},"DateTimeField",[826,4795,1289],{"class":1178},[826,4797,4798],{"class":1302},"null",[826,4800,4531],{"class":1178},[826,4802,4528],{"class":1302},[826,4804,4537],{"class":1178},[826,4806,4807,4810,4812,4814,4816,4818,4820,4822,4824,4826],{"class":1047,"line":2061},[826,4808,4809],{"class":1051},"    invite_sent_at ",[826,4811,1179],{"class":1178},[826,4813,4419],{"class":1051},[826,4815,1282],{"class":1178},[826,4817,4793],{"class":1285},[826,4819,1289],{"class":1178},[826,4821,4798],{"class":1302},[826,4823,4531],{"class":1178},[826,4825,4528],{"class":1302},[826,4827,4537],{"class":1178},[826,4829,4830,4833,4835,4837,4839,4841,4843,4845,4847,4849],{"class":1047,"line":2071},[826,4831,4832],{"class":1051},"    signed_at ",[826,4834,1179],{"class":1178},[826,4836,4419],{"class":1051},[826,4838,1282],{"class":1178},[826,4840,4793],{"class":1285},[826,4842,1289],{"class":1178},[826,4844,4798],{"class":1302},[826,4846,4531],{"class":1178},[826,4848,4528],{"class":1302},[826,4850,4537],{"class":1178},[826,4852,4853,4856,4858,4860,4862,4864,4866,4868,4870,4872],{"class":1047,"line":2079},[826,4854,4855],{"class":1051},"    downloaded_at ",[826,4857,1179],{"class":1178},[826,4859,4419],{"class":1051},[826,4861,1282],{"class":1178},[826,4863,4793],{"class":1285},[826,4865,1289],{"class":1178},[826,4867,4798],{"class":1302},[826,4869,4531],{"class":1178},[826,4871,4528],{"class":1302},[826,4873,4537],{"class":1178},[746,4875,4876],{},"The status flow is linear and predictable:",[1037,4878,4883],{"className":4879,"code":4881,"language":4882},[4880],"language-text","PENDING → UPLOADED → INVITE_SENT → SIGNED → DOWNLOADED\n                                                ↘ FAILED (at any step)\n","text",[1043,4884,4881],{"__ignoreMap":865},[753,4886,4888],{"id":4887},"step-4-build-the-async-signing-pipeline-with-celery","Step 4: Build the async signing pipeline with Celery",[746,4890,4891],{},"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:",[1037,4893,4896],{"className":1259,"code":4894,"filename":4895,"language":1262,"meta":865,"style":865},"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",[1043,4897,4898,4910,4914,4919,4928,4945,4961,4977,4993,5009,5028,5048,5061,5081,5085,5089,5093,5127,5152,5161,5178,5199,5203,5237,5247,5252,5256,5263,5268,5283,5305,5345,5349,5354,5369,5394,5406,5425,5429,5452,5474,5495,5537,5541,5546,5558,5568,5578,5582,5586,5591,5616,5644,5702,5706,5711,5721,5741,5759,5784,5800,5805,5809,5825,5840,5852,5868,5873,5878,5901,5921,5952,5957,5963,5979,5988,6001,6018,6034,6047,6052,6057,6073,6103,6125,6146,6177,6183],{"__ignoreMap":865},[826,4899,4900,4902,4905,4907],{"class":1047,"line":1048},[826,4901,1508],{"class":1499},[826,4903,4904],{"class":1051}," celery ",[826,4906,1500],{"class":1499},[826,4908,4909],{"class":1051}," shared_task\n",[826,4911,4912],{"class":1047,"line":866},[826,4913,1547],{"emptyLinePlaceholder":877},[826,4915,4916],{"class":1047,"line":1060},[826,4917,4918],{"class":1169},"# Signature field position (calibrate for your PDF layout)\n",[826,4920,4921,4924,4926],{"class":1047,"line":1066},[826,4922,4923],{"class":1051},"TESTER_SIGNATURE_FIELD ",[826,4925,1179],{"class":1178},[826,4927,3815],{"class":1178},[826,4929,4930,4933,4936,4938,4940,4943],{"class":1047,"line":1072},[826,4931,4932],{"class":1178},"    \"",[826,4934,4935],{"class":1182},"x",[826,4937,1292],{"class":1178},[826,4939,2182],{"class":1178},[826,4941,4942],{"class":1573}," 350",[826,4944,2159],{"class":1178},[826,4946,4947,4949,4952,4954,4956,4959],{"class":1047,"line":1218},[826,4948,4932],{"class":1178},[826,4950,4951],{"class":1182},"y",[826,4953,1292],{"class":1178},[826,4955,2182],{"class":1178},[826,4957,4958],{"class":1573}," 700",[826,4960,2159],{"class":1178},[826,4962,4963,4965,4968,4970,4972,4975],{"class":1047,"line":1229},[826,4964,4932],{"class":1178},[826,4966,4967],{"class":1182},"width",[826,4969,1292],{"class":1178},[826,4971,2182],{"class":1178},[826,4973,4974],{"class":1573}," 200",[826,4976,2159],{"class":1178},[826,4978,4979,4981,4984,4986,4988,4991],{"class":1047,"line":1584},[826,4980,4932],{"class":1178},[826,4982,4983],{"class":1182},"height",[826,4985,1292],{"class":1178},[826,4987,2182],{"class":1178},[826,4989,4990],{"class":1573}," 50",[826,4992,2159],{"class":1178},[826,4994,4995,4997,5000,5002,5004,5007],{"class":1047,"line":1589},[826,4996,4932],{"class":1178},[826,4998,4999],{"class":1182},"page_number",[826,5001,1292],{"class":1178},[826,5003,2182],{"class":1178},[826,5005,5006],{"class":1573}," 0",[826,5008,2159],{"class":1178},[826,5010,5011,5013,5015,5017,5019,5021,5024,5026],{"class":1047,"line":1603},[826,5012,4932],{"class":1178},[826,5014,3236],{"class":1182},[826,5016,1292],{"class":1178},[826,5018,2182],{"class":1178},[826,5020,1557],{"class":1178},[826,5022,5023],{"class":1182},"signature",[826,5025,1292],{"class":1178},[826,5027,2159],{"class":1178},[826,5029,5030,5032,5035,5037,5039,5041,5044,5046],{"class":1047,"line":1615},[826,5031,4932],{"class":1178},[826,5033,5034],{"class":1182},"role",[826,5036,1292],{"class":1178},[826,5038,2182],{"class":1178},[826,5040,1557],{"class":1178},[826,5042,5043],{"class":1182},"Signer 1",[826,5045,1292],{"class":1178},[826,5047,2159],{"class":1178},[826,5049,5050,5052,5055,5057,5059],{"class":1047,"line":1620},[826,5051,4932],{"class":1178},[826,5053,5054],{"class":1182},"required",[826,5056,1292],{"class":1178},[826,5058,2182],{"class":1178},[826,5060,3908],{"class":1178},[826,5062,5063,5065,5068,5070,5072,5074,5077,5079],{"class":1047,"line":1631},[826,5064,4932],{"class":1178},[826,5066,5067],{"class":1182},"label",[826,5069,1292],{"class":1178},[826,5071,2182],{"class":1178},[826,5073,1557],{"class":1178},[826,5075,5076],{"class":1182},"Tester Signature",[826,5078,1292],{"class":1178},[826,5080,2159],{"class":1178},[826,5082,5083],{"class":1047,"line":1642},[826,5084,3495],{"class":1178},[826,5086,5087],{"class":1047,"line":1652},[826,5088,1547],{"emptyLinePlaceholder":877},[826,5090,5091],{"class":1047,"line":1662},[826,5092,1547],{"emptyLinePlaceholder":877},[826,5094,5095,5097,5100,5102,5105,5107,5110,5112,5115,5117,5120,5122,5125],{"class":1047,"line":1672},[826,5096,2534],{"class":1178},[826,5098,5099],{"class":1285},"shared_task",[826,5101,1289],{"class":1178},[826,5103,5104],{"class":1302},"bind",[826,5106,4531],{"class":1178},[826,5108,5109],{"class":1302}," max_retries",[826,5111,1179],{"class":1178},[826,5113,5114],{"class":1573},"3",[826,5116,1299],{"class":1178},[826,5118,5119],{"class":1302}," default_retry_delay",[826,5121,1179],{"class":1178},[826,5123,5124],{"class":1573},"60",[826,5126,1314],{"class":1178},[826,5128,5129,5131,5134,5136,5140,5142,5145,5147,5150],{"class":1047,"line":1677},[826,5130,2541],{"class":1592},[826,5132,5133],{"class":1285}," upload_and_send_for_signature",[826,5135,1289],{"class":1178},[826,5137,5139],{"class":5138},"s5tWE","self",[826,5141,1299],{"class":1178},[826,5143,5144],{"class":1302}," signable_document_id",[826,5146,2182],{"class":1178},[826,5148,5149],{"class":1596}," int",[826,5151,4230],{"class":1178},[826,5153,5154,5156,5159],{"class":1047,"line":1686},[826,5155,1606],{"class":1499},[826,5157,5158],{"class":1169},"Full pipeline: upload PDF → add fields → send invite → register webhook.",[826,5160,1612],{"class":1499},[826,5162,5163,5166,5169,5171,5173,5175],{"class":1047,"line":1710},[826,5164,5165],{"class":1499},"    from",[826,5167,5168],{"class":1051}," signnow",[826,5170,1282],{"class":1178},[826,5172,4181],{"class":1051},[826,5174,1500],{"class":1499},[826,5176,5177],{"class":1051}," SignableDocument\n",[826,5179,5180,5182,5184,5186,5189,5191,5194,5196],{"class":1047,"line":1721},[826,5181,5165],{"class":1499},[826,5183,5168],{"class":1051},[826,5185,1282],{"class":1178},[826,5187,5188],{"class":1051},"services",[826,5190,1282],{"class":1178},[826,5192,5193],{"class":1051},"signnow_service ",[826,5195,1500],{"class":1499},[826,5197,5198],{"class":1051}," SignNowService\n",[826,5200,5201],{"class":1047,"line":1738},[826,5202,1547],{"emptyLinePlaceholder":877},[826,5204,5205,5208,5210,5212,5214,5217,5219,5222,5224,5226,5228,5230,5233,5235],{"class":1047,"line":1747},[826,5206,5207],{"class":1051},"    signable ",[826,5209,1179],{"class":1178},[826,5211,4217],{"class":1051},[826,5213,1282],{"class":1178},[826,5215,5216],{"class":1732},"objects",[826,5218,1282],{"class":1178},[826,5220,5221],{"class":1285},"select_related",[826,5223,1289],{"class":1178},[826,5225,1292],{"class":1178},[826,5227,4477],{"class":1182},[826,5229,1292],{"class":1178},[826,5231,5232],{"class":1178},").",[826,5234,2051],{"class":1285},[826,5236,2133],{"class":1178},[826,5238,5239,5242,5244],{"class":1047,"line":1752},[826,5240,5241],{"class":1302},"        id",[826,5243,1179],{"class":1178},[826,5245,5246],{"class":1285},"signable_document_id\n",[826,5248,5249],{"class":1047,"line":1789},[826,5250,5251],{"class":1178},"    )\n",[826,5253,5254],{"class":1047,"line":1821},[826,5255,1547],{"emptyLinePlaceholder":877},[826,5257,5258,5261],{"class":1047,"line":1853},[826,5259,5260],{"class":1499},"    try",[826,5262,1600],{"class":1178},[826,5264,5265],{"class":1047,"line":1885},[826,5266,5267],{"class":1169},"        # 1. Read PDF from storage\n",[826,5269,5270,5273,5275,5278,5280],{"class":1047,"line":1890},[826,5271,5272],{"class":1051},"        source ",[826,5274,1179],{"class":1178},[826,5276,5277],{"class":1051}," signable",[826,5279,1282],{"class":1178},[826,5281,5282],{"class":1732},"source_document\n",[826,5284,5285,5288,5290,5293,5295,5298,5300,5303],{"class":1047,"line":1937},[826,5286,5287],{"class":1051},"        pdf_bytes ",[826,5289,1179],{"class":1178},[826,5291,5292],{"class":1051}," source",[826,5294,1282],{"class":1178},[826,5296,5297],{"class":1732},"pdf_file",[826,5299,1282],{"class":1178},[826,5301,5302],{"class":1285},"read",[826,5304,2343],{"class":1178},[826,5306,5307,5310,5312,5314,5316,5318,5320,5323,5325,5328,5330,5332,5335,5337,5340,5343],{"class":1047,"line":1958},[826,5308,5309],{"class":1051},"        filename ",[826,5311,1179],{"class":1178},[826,5313,5292],{"class":1051},[826,5315,1282],{"class":1178},[826,5317,5297],{"class":1732},[826,5319,1282],{"class":1178},[826,5321,5322],{"class":1732},"name",[826,5324,1282],{"class":1178},[826,5326,5327],{"class":1285},"split",[826,5329,1289],{"class":1178},[826,5331,1292],{"class":1178},[826,5333,5334],{"class":1182},"/",[826,5336,1292],{"class":1178},[826,5338,5339],{"class":1178},")[-",[826,5341,5342],{"class":1573},"1",[826,5344,2390],{"class":1178},[826,5346,5347],{"class":1047,"line":1965},[826,5348,1547],{"emptyLinePlaceholder":877},[826,5350,5351],{"class":1047,"line":1970},[826,5352,5353],{"class":1169},"        # 2. Upload to SignNow\n",[826,5355,5356,5358,5360,5362,5364,5367],{"class":1047,"line":1983},[826,5357,1724],{"class":1499},[826,5359,1895],{"class":1178},[826,5361,5277],{"class":1051},[826,5363,1282],{"class":1178},[826,5365,5366],{"class":1732},"signnow_document_id",[826,5368,1600],{"class":1178},[826,5370,5371,5374,5376,5378,5380,5383,5385,5388,5390,5392],{"class":1047,"line":1991},[826,5372,5373],{"class":1051},"            result ",[826,5375,1179],{"class":1178},[826,5377,1597],{"class":1051},[826,5379,1282],{"class":1178},[826,5381,5382],{"class":1285},"upload_document",[826,5384,1289],{"class":1178},[826,5386,5387],{"class":1285},"pdf_bytes",[826,5389,1299],{"class":1178},[826,5391,2563],{"class":1285},[826,5393,1314],{"class":1178},[826,5395,5396,5399,5401,5404],{"class":1047,"line":1996},[826,5397,5398],{"class":1499},"            if",[826,5400,1895],{"class":1178},[826,5402,5403],{"class":1051}," result",[826,5405,1600],{"class":1178},[826,5407,5408,5411,5414,5416,5418,5421,5423],{"class":1047,"line":2003},[826,5409,5410],{"class":1499},"                raise",[826,5412,5413],{"class":1596}," RuntimeError",[826,5415,1289],{"class":1178},[826,5417,1292],{"class":1178},[826,5419,5420],{"class":1182},"Failed to upload document to SignNow",[826,5422,1292],{"class":1178},[826,5424,1314],{"class":1178},[826,5426,5427],{"class":1047,"line":2028},[826,5428,1547],{"emptyLinePlaceholder":877},[826,5430,5431,5434,5436,5438,5440,5442,5444,5446,5448,5450],{"class":1047,"line":2038},[826,5432,5433],{"class":1051},"            signable",[826,5435,1282],{"class":1178},[826,5437,5366],{"class":1732},[826,5439,1763],{"class":1178},[826,5441,5403],{"class":1051},[826,5443,2380],{"class":1178},[826,5445,1292],{"class":1178},[826,5447,2779],{"class":1182},[826,5449,1292],{"class":1178},[826,5451,2390],{"class":1178},[826,5453,5454,5456,5458,5461,5463,5465,5467,5469,5471],{"class":1047,"line":2061},[826,5455,5433],{"class":1051},[826,5457,1282],{"class":1178},[826,5459,5460],{"class":1732},"status",[826,5462,1763],{"class":1178},[826,5464,4217],{"class":1051},[826,5466,1282],{"class":1178},[826,5468,4658],{"class":1732},[826,5470,1282],{"class":1178},[826,5472,5473],{"class":1732},"UPLOADED\n",[826,5475,5476,5478,5480,5483,5485,5488,5490,5493],{"class":1047,"line":2071},[826,5477,5433],{"class":1051},[826,5479,1282],{"class":1178},[826,5481,5482],{"class":1732},"uploaded_at",[826,5484,1763],{"class":1178},[826,5486,5487],{"class":1051}," timezone",[826,5489,1282],{"class":1178},[826,5491,5492],{"class":1285},"now",[826,5494,2343],{"class":1178},[826,5496,5497,5499,5501,5504,5506,5509,5512,5514,5516,5518,5520,5522,5524,5526,5528,5530,5532,5534],{"class":1047,"line":2079},[826,5498,5433],{"class":1051},[826,5500,1282],{"class":1178},[826,5502,5503],{"class":1285},"save",[826,5505,1289],{"class":1178},[826,5507,5508],{"class":1302},"update_fields",[826,5510,5511],{"class":1178},"=[",[826,5513,1292],{"class":1178},[826,5515,5366],{"class":1182},[826,5517,1292],{"class":1178},[826,5519,1299],{"class":1178},[826,5521,1557],{"class":1178},[826,5523,5460],{"class":1182},[826,5525,1292],{"class":1178},[826,5527,1299],{"class":1178},[826,5529,1557],{"class":1178},[826,5531,5482],{"class":1182},[826,5533,1292],{"class":1178},[826,5535,5536],{"class":1178},"])\n",[826,5538,5539],{"class":1047,"line":2084},[826,5540,1547],{"emptyLinePlaceholder":877},[826,5542,5543],{"class":1047,"line":2118},[826,5544,5545],{"class":1169},"        # 3. Add signature fields\n",[826,5547,5548,5551,5553,5556],{"class":1047,"line":2136},[826,5549,5550],{"class":1051},"        SignNowService",[826,5552,1282],{"class":1178},[826,5554,5555],{"class":1285},"add_signature_fields",[826,5557,2133],{"class":1178},[826,5559,5560,5562,5564,5566],{"class":1047,"line":2162},[826,5561,5433],{"class":1285},[826,5563,1282],{"class":1178},[826,5565,5366],{"class":1732},[826,5567,2159],{"class":1178},[826,5569,5570,5573,5576],{"class":1047,"line":2171},[826,5571,5572],{"class":1178},"            [",[826,5574,5575],{"class":1285},"TESTER_SIGNATURE_FIELD",[826,5577,3352],{"class":1178},[826,5579,5580],{"class":1047,"line":2194},[826,5581,2756],{"class":1178},[826,5583,5584],{"class":1047,"line":2214},[826,5585,1547],{"emptyLinePlaceholder":877},[826,5587,5588],{"class":1047,"line":2233},[826,5589,5590],{"class":1169},"        # 4. Get role_id (assigned when fields were added)\n",[826,5592,5593,5596,5598,5600,5602,5605,5607,5610,5612,5614],{"class":1047,"line":2254},[826,5594,5595],{"class":1051},"        doc_data ",[826,5597,1179],{"class":1178},[826,5599,1597],{"class":1051},[826,5601,1282],{"class":1178},[826,5603,5604],{"class":1285},"get_document",[826,5606,1289],{"class":1178},[826,5608,5609],{"class":1285},"signable",[826,5611,1282],{"class":1178},[826,5613,5366],{"class":1732},[826,5615,1314],{"class":1178},[826,5617,5618,5621,5623,5626,5628,5630,5632,5634,5637,5639,5641],{"class":1047,"line":2260},[826,5619,5620],{"class":1051},"        roles ",[826,5622,1179],{"class":1178},[826,5624,5625],{"class":1051}," doc_data",[826,5627,1282],{"class":1178},[826,5629,2051],{"class":1285},[826,5631,1289],{"class":1178},[826,5633,1292],{"class":1178},[826,5635,5636],{"class":1182},"roles",[826,5638,1292],{"class":1178},[826,5640,1299],{"class":1178},[826,5642,5643],{"class":1178}," [])\n",[826,5645,5646,5649,5651,5654,5656,5659,5662,5665,5668,5671,5674,5677,5679,5681,5683,5685,5687,5689,5691,5694,5696,5698,5700],{"class":1047,"line":2268},[826,5647,5648],{"class":1051},"        signer_role ",[826,5650,1179],{"class":1178},[826,5652,5653],{"class":1285}," next",[826,5655,1289],{"class":1178},[826,5657,5658],{"class":1285},"r ",[826,5660,5661],{"class":1499},"for",[826,5663,5664],{"class":1285}," r ",[826,5666,5667],{"class":1499},"in",[826,5669,5670],{"class":1285}," roles ",[826,5672,5673],{"class":1499},"if",[826,5675,5676],{"class":1285}," r",[826,5678,1282],{"class":1178},[826,5680,2051],{"class":1285},[826,5682,1289],{"class":1178},[826,5684,1292],{"class":1178},[826,5686,5322],{"class":1182},[826,5688,1292],{"class":1178},[826,5690,1150],{"class":1178},[826,5692,5693],{"class":1178}," ==",[826,5695,1557],{"class":1178},[826,5697,5043],{"class":1182},[826,5699,1292],{"class":1178},[826,5701,1314],{"class":1178},[826,5703,5704],{"class":1047,"line":2300},[826,5705,1547],{"emptyLinePlaceholder":877},[826,5707,5708],{"class":1047,"line":2321},[826,5709,5710],{"class":1169},"        # 5. Send role-based invite\n",[826,5712,5713,5716,5718],{"class":1047,"line":2326},[826,5714,5715],{"class":1051},"        signers ",[826,5717,1179],{"class":1178},[826,5719,5720],{"class":1178}," [{\n",[826,5722,5723,5725,5728,5730,5732,5734,5736,5739],{"class":1047,"line":2332},[826,5724,3884],{"class":1178},[826,5726,5727],{"class":1182},"email",[826,5729,1292],{"class":1178},[826,5731,2182],{"class":1178},[826,5733,5277],{"class":1051},[826,5735,1282],{"class":1178},[826,5737,5738],{"class":1732},"signer_email",[826,5740,2159],{"class":1178},[826,5742,5743,5745,5747,5749,5751,5753,5755,5757],{"class":1047,"line":2346},[826,5744,3884],{"class":1178},[826,5746,5034],{"class":1182},[826,5748,1292],{"class":1178},[826,5750,2182],{"class":1178},[826,5752,1557],{"class":1178},[826,5754,5043],{"class":1182},[826,5756,1292],{"class":1178},[826,5758,2159],{"class":1178},[826,5760,5761,5763,5766,5768,5770,5773,5775,5777,5780,5782],{"class":1047,"line":2364},[826,5762,3884],{"class":1178},[826,5764,5765],{"class":1182},"role_id",[826,5767,1292],{"class":1178},[826,5769,2182],{"class":1178},[826,5771,5772],{"class":1051}," signer_role",[826,5774,2380],{"class":1178},[826,5776,1292],{"class":1178},[826,5778,5779],{"class":1182},"unique_id",[826,5781,1292],{"class":1178},[826,5783,3352],{"class":1178},[826,5785,5786,5788,5791,5793,5795,5798],{"class":1047,"line":2369},[826,5787,3884],{"class":1178},[826,5789,5790],{"class":1182},"order",[826,5792,1292],{"class":1178},[826,5794,2182],{"class":1178},[826,5796,5797],{"class":1573}," 1",[826,5799,2159],{"class":1178},[826,5801,5802],{"class":1047,"line":2393},[826,5803,5804],{"class":1178},"        }]\n",[826,5806,5807],{"class":1047,"line":2423},[826,5808,1547],{"emptyLinePlaceholder":877},[826,5810,5811,5814,5816,5818,5820,5823],{"class":1047,"line":2452},[826,5812,5813],{"class":1051},"        invite_result ",[826,5815,1179],{"class":1178},[826,5817,1597],{"class":1051},[826,5819,1282],{"class":1178},[826,5821,5822],{"class":1285},"send_role_based_invite",[826,5824,2133],{"class":1178},[826,5826,5827,5830,5832,5834,5836,5838],{"class":1047,"line":2484},[826,5828,5829],{"class":1302},"            document_id",[826,5831,1179],{"class":1178},[826,5833,5609],{"class":1285},[826,5835,1282],{"class":1178},[826,5837,5366],{"class":1732},[826,5839,2159],{"class":1178},[826,5841,5842,5845,5847,5850],{"class":1047,"line":2489},[826,5843,5844],{"class":1302},"            signers",[826,5846,1179],{"class":1178},[826,5848,5849],{"class":1285},"signers",[826,5851,2159],{"class":1178},[826,5853,5855,5858,5860,5862,5864,5866],{"class":1047,"line":5854},66,[826,5856,5857],{"class":1302},"            from_email",[826,5859,1179],{"class":1178},[826,5861,1771],{"class":1285},[826,5863,1282],{"class":1178},[826,5865,1200],{"class":1732},[826,5867,2159],{"class":1178},[826,5869,5871],{"class":1047,"line":5870},67,[826,5872,2756],{"class":1178},[826,5874,5876],{"class":1047,"line":5875},68,[826,5877,1547],{"emptyLinePlaceholder":877},[826,5879,5881,5884,5886,5888,5890,5892,5894,5896,5898],{"class":1047,"line":5880},69,[826,5882,5883],{"class":1051},"        signable",[826,5885,1282],{"class":1178},[826,5887,5460],{"class":1732},[826,5889,1763],{"class":1178},[826,5891,4217],{"class":1051},[826,5893,1282],{"class":1178},[826,5895,4658],{"class":1732},[826,5897,1282],{"class":1178},[826,5899,5900],{"class":1732},"INVITE_SENT\n",[826,5902,5904,5906,5908,5911,5913,5915,5917,5919],{"class":1047,"line":5903},70,[826,5905,5883],{"class":1051},[826,5907,1282],{"class":1178},[826,5909,5910],{"class":1732},"invite_sent_at",[826,5912,1763],{"class":1178},[826,5914,5487],{"class":1051},[826,5916,1282],{"class":1178},[826,5918,5492],{"class":1285},[826,5920,2343],{"class":1178},[826,5922,5924,5926,5928,5930,5932,5934,5936,5938,5940,5942,5944,5946,5948,5950],{"class":1047,"line":5923},71,[826,5925,5883],{"class":1051},[826,5927,1282],{"class":1178},[826,5929,5503],{"class":1285},[826,5931,1289],{"class":1178},[826,5933,5508],{"class":1302},[826,5935,5511],{"class":1178},[826,5937,1292],{"class":1178},[826,5939,5460],{"class":1182},[826,5941,1292],{"class":1178},[826,5943,1299],{"class":1178},[826,5945,1557],{"class":1178},[826,5947,5910],{"class":1182},[826,5949,1292],{"class":1178},[826,5951,5536],{"class":1178},[826,5953,5955],{"class":1047,"line":5954},72,[826,5956,1547],{"emptyLinePlaceholder":877},[826,5958,5960],{"class":1047,"line":5959},73,[826,5961,5962],{"class":1169},"        # 6. Register webhook for document completion\n",[826,5964,5966,5969,5971,5974,5976],{"class":1047,"line":5965},74,[826,5967,5968],{"class":1051},"        callback_url ",[826,5970,1179],{"class":1178},[826,5972,5973],{"class":1051}," settings",[826,5975,1282],{"class":1178},[826,5977,5978],{"class":1732},"SIGNNOW_WEBHOOK_CALLBACK_URL\n",[826,5980,5982,5984,5986],{"class":1047,"line":5981},75,[826,5983,1724],{"class":1499},[826,5985,3751],{"class":1051},[826,5987,1600],{"class":1178},[826,5989,5991,5994,5996,5999],{"class":1047,"line":5990},76,[826,5992,5993],{"class":1051},"            SignNowService",[826,5995,1282],{"class":1178},[826,5997,5998],{"class":1285},"register_webhook",[826,6000,2133],{"class":1178},[826,6002,6004,6007,6009,6011,6014,6016],{"class":1047,"line":6003},77,[826,6005,6006],{"class":1302},"                event",[826,6008,1179],{"class":1178},[826,6010,1292],{"class":1178},[826,6012,6013],{"class":1182},"document.complete",[826,6015,1292],{"class":1178},[826,6017,2159],{"class":1178},[826,6019,6021,6024,6026,6028,6030,6032],{"class":1047,"line":6020},78,[826,6022,6023],{"class":1302},"                entity_id",[826,6025,1179],{"class":1178},[826,6027,5609],{"class":1285},[826,6029,1282],{"class":1178},[826,6031,5366],{"class":1732},[826,6033,2159],{"class":1178},[826,6035,6037,6040,6042,6045],{"class":1047,"line":6036},79,[826,6038,6039],{"class":1302},"                callback_url",[826,6041,1179],{"class":1178},[826,6043,6044],{"class":1285},"callback_url",[826,6046,2159],{"class":1178},[826,6048,6050],{"class":1047,"line":6049},80,[826,6051,2329],{"class":1178},[826,6053,6055],{"class":1047,"line":6054},81,[826,6056,1547],{"emptyLinePlaceholder":877},[826,6058,6060,6063,6066,6068,6071],{"class":1047,"line":6059},82,[826,6061,6062],{"class":1499},"    except",[826,6064,6065],{"class":1596}," Exception",[826,6067,2110],{"class":1499},[826,6069,6070],{"class":1051}," exc",[826,6072,1600],{"class":1178},[826,6074,6076,6078,6081,6083,6086,6088,6091,6094,6096,6098,6101],{"class":1047,"line":6075},83,[826,6077,1724],{"class":1499},[826,6079,6080],{"class":1051}," self",[826,6082,1282],{"class":1178},[826,6084,6085],{"class":1732},"request",[826,6087,1282],{"class":1178},[826,6089,6090],{"class":1732},"retries",[826,6092,6093],{"class":1178}," >=",[826,6095,6080],{"class":1051},[826,6097,1282],{"class":1178},[826,6099,6100],{"class":1732},"max_retries",[826,6102,1600],{"class":1178},[826,6104,6106,6108,6110,6112,6114,6116,6118,6120,6122],{"class":1047,"line":6105},84,[826,6107,5433],{"class":1051},[826,6109,1282],{"class":1178},[826,6111,5460],{"class":1732},[826,6113,1763],{"class":1178},[826,6115,4217],{"class":1051},[826,6117,1282],{"class":1178},[826,6119,4658],{"class":1732},[826,6121,1282],{"class":1178},[826,6123,6124],{"class":1732},"FAILED\n",[826,6126,6128,6130,6132,6135,6137,6139,6141,6144],{"class":1047,"line":6127},85,[826,6129,5433],{"class":1051},[826,6131,1282],{"class":1178},[826,6133,6134],{"class":1732},"error_message",[826,6136,1763],{"class":1178},[826,6138,2019],{"class":1596},[826,6140,1289],{"class":1178},[826,6142,6143],{"class":1285},"exc",[826,6145,1314],{"class":1178},[826,6147,6149,6151,6153,6155,6157,6159,6161,6163,6165,6167,6169,6171,6173,6175],{"class":1047,"line":6148},86,[826,6150,5433],{"class":1051},[826,6152,1282],{"class":1178},[826,6154,5503],{"class":1285},[826,6156,1289],{"class":1178},[826,6158,5508],{"class":1302},[826,6160,5511],{"class":1178},[826,6162,1292],{"class":1178},[826,6164,5460],{"class":1182},[826,6166,1292],{"class":1178},[826,6168,1299],{"class":1178},[826,6170,1557],{"class":1178},[826,6172,6134],{"class":1182},[826,6174,1292],{"class":1178},[826,6176,5536],{"class":1178},[826,6178,6180],{"class":1047,"line":6179},87,[826,6181,6182],{"class":1499},"            return\n",[826,6184,6186,6189,6191,6194,6196,6198,6200,6202],{"class":1047,"line":6185},88,[826,6187,6188],{"class":1051},"        self",[826,6190,1282],{"class":1178},[826,6192,6193],{"class":1285},"retry",[826,6195,1289],{"class":1178},[826,6197,6143],{"class":1302},[826,6199,1179],{"class":1178},[826,6201,6143],{"class":1285},[826,6203,1314],{"class":1178},[6205,6206,6207],"note",{},[746,6208,1243,6209,6211,6212,6214,6215,6214,6217,6214,6219,6221,6222,6225],{},[1043,6210,5575],{}," coordinates (",[1043,6213,4935],{},", ",[1043,6216,4951],{},[1043,6218,4967],{},[1043,6220,4983],{},") 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 ",[1043,6223,6224],{},"(0,0)"," is the top-left corner of the page.",[753,6227,6229],{"id":6228},"step-5-handle-webhooks","Step 5: Handle webhooks",[746,6231,6232],{},"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:",[1037,6234,6237],{"className":1259,"code":6235,"filename":6236,"language":1262,"meta":865,"style":865},"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",[1043,6238,6239,6246,6253,6257,6274,6290,6306,6310,6314,6328,6337,6341,6356,6366,6370,6388,6393,6420,6428,6461,6478,6499,6511,6523,6533,6537,6562,6601,6605,6610,6642,6673,6701,6705,6733,6752,6763,6786,6790,6811,6830,6860,6864,6869,6889,6893,6898],{"__ignoreMap":865},[826,6240,6241,6243],{"class":1047,"line":1048},[826,6242,1500],{"class":1499},[826,6244,6245],{"class":1051}," hashlib\n",[826,6247,6248,6250],{"class":1047,"line":866},[826,6249,1500],{"class":1499},[826,6251,6252],{"class":1051}," hmac\n",[826,6254,6255],{"class":1047,"line":1060},[826,6256,1547],{"emptyLinePlaceholder":877},[826,6258,6259,6261,6264,6266,6269,6271],{"class":1047,"line":1066},[826,6260,1508],{"class":1499},[826,6262,6263],{"class":1051}," rest_framework",[826,6265,1282],{"class":1178},[826,6267,6268],{"class":1051},"permissions ",[826,6270,1500],{"class":1499},[826,6272,6273],{"class":1051}," AllowAny\n",[826,6275,6276,6278,6280,6282,6285,6287],{"class":1047,"line":1072},[826,6277,1508],{"class":1499},[826,6279,6263],{"class":1051},[826,6281,1282],{"class":1178},[826,6283,6284],{"class":1051},"response ",[826,6286,1500],{"class":1499},[826,6288,6289],{"class":1051}," Response\n",[826,6291,6292,6294,6296,6298,6301,6303],{"class":1047,"line":1218},[826,6293,1508],{"class":1499},[826,6295,6263],{"class":1051},[826,6297,1282],{"class":1178},[826,6299,6300],{"class":1051},"views ",[826,6302,1500],{"class":1499},[826,6304,6305],{"class":1051}," APIView\n",[826,6307,6308],{"class":1047,"line":1229},[826,6309,1547],{"emptyLinePlaceholder":877},[826,6311,6312],{"class":1047,"line":1584},[826,6313,1547],{"emptyLinePlaceholder":877},[826,6315,6316,6318,6321,6323,6326],{"class":1047,"line":1589},[826,6317,1593],{"class":1592},[826,6319,6320],{"class":1596}," SignNowWebhookView",[826,6322,1289],{"class":1178},[826,6324,6325],{"class":1596},"APIView",[826,6327,4230],{"class":1178},[826,6329,6330,6332,6335],{"class":1047,"line":1603},[826,6331,1606],{"class":1499},[826,6333,6334],{"class":1169},"Receives webhook callbacks from SignNow when documents are signed.",[826,6336,1612],{"class":1499},[826,6338,6339],{"class":1047,"line":1615},[826,6340,1547],{"emptyLinePlaceholder":877},[826,6342,6343,6346,6348,6351,6354],{"class":1047,"line":1620},[826,6344,6345],{"class":1051},"    permission_classes ",[826,6347,1179],{"class":1178},[826,6349,6350],{"class":1178}," [",[826,6352,6353],{"class":1051},"AllowAny",[826,6355,2390],{"class":1178},[826,6357,6358,6361,6363],{"class":1047,"line":1631},[826,6359,6360],{"class":1051},"    authentication_classes ",[826,6362,1179],{"class":1178},[826,6364,6365],{"class":1178}," []\n",[826,6367,6368],{"class":1047,"line":1642},[826,6369,1547],{"emptyLinePlaceholder":877},[826,6371,6372,6374,6377,6379,6381,6383,6386],{"class":1047,"line":1652},[826,6373,1689],{"class":1592},[826,6375,6376],{"class":1285}," post",[826,6378,1289],{"class":1178},[826,6380,5139],{"class":5138},[826,6382,1299],{"class":1178},[826,6384,6385],{"class":1302}," request",[826,6387,4230],{"class":1178},[826,6389,6390],{"class":1047,"line":1662},[826,6391,6392],{"class":1169},"        # Verify webhook signature\n",[826,6394,6395,6398,6400,6402,6404,6406,6408,6410,6412,6414,6416,6418],{"class":1047,"line":1672},[826,6396,6397],{"class":1051},"        webhook_secret ",[826,6399,1179],{"class":1178},[826,6401,1766],{"class":1285},[826,6403,1289],{"class":1178},[826,6405,1771],{"class":1285},[826,6407,1299],{"class":1178},[826,6409,1557],{"class":1178},[826,6411,1221],{"class":1182},[826,6413,1292],{"class":1178},[826,6415,1299],{"class":1178},[826,6417,1784],{"class":1178},[826,6419,1314],{"class":1178},[826,6421,6422,6424,6426],{"class":1047,"line":1677},[826,6423,1724],{"class":1499},[826,6425,3956],{"class":1051},[826,6427,1600],{"class":1178},[826,6429,6430,6433,6435,6437,6439,6442,6444,6446,6448,6450,6453,6455,6457,6459],{"class":1047,"line":1686},[826,6431,6432],{"class":1051},"            received_signature ",[826,6434,1179],{"class":1178},[826,6436,6385],{"class":1051},[826,6438,1282],{"class":1178},[826,6440,6441],{"class":1732},"headers",[826,6443,1282],{"class":1178},[826,6445,2051],{"class":1285},[826,6447,1289],{"class":1178},[826,6449,1292],{"class":1178},[826,6451,6452],{"class":1182},"X-SignNow-Signature",[826,6454,1292],{"class":1178},[826,6456,1299],{"class":1178},[826,6458,1784],{"class":1178},[826,6460,1314],{"class":1178},[826,6462,6463,6466,6468,6471,6473,6476],{"class":1047,"line":1710},[826,6464,6465],{"class":1051},"            expected ",[826,6467,1179],{"class":1178},[826,6469,6470],{"class":1051}," hmac",[826,6472,1282],{"class":1178},[826,6474,6475],{"class":1285},"new",[826,6477,2133],{"class":1178},[826,6479,6480,6483,6485,6488,6490,6492,6495,6497],{"class":1047,"line":1721},[826,6481,6482],{"class":1285},"                webhook_secret",[826,6484,1282],{"class":1178},[826,6486,6487],{"class":1285},"encode",[826,6489,1289],{"class":1178},[826,6491,1292],{"class":1178},[826,6493,6494],{"class":1182},"utf-8",[826,6496,1292],{"class":1178},[826,6498,2712],{"class":1178},[826,6500,6501,6504,6506,6509],{"class":1047,"line":1738},[826,6502,6503],{"class":1285},"                request",[826,6505,1282],{"class":1178},[826,6507,6508],{"class":1732},"body",[826,6510,2159],{"class":1178},[826,6512,6513,6516,6518,6521],{"class":1047,"line":1747},[826,6514,6515],{"class":1285},"                hashlib",[826,6517,1282],{"class":1178},[826,6519,6520],{"class":1732},"sha256",[826,6522,2159],{"class":1178},[826,6524,6525,6528,6531],{"class":1047,"line":1752},[826,6526,6527],{"class":1178},"            ).",[826,6529,6530],{"class":1285},"hexdigest",[826,6532,2343],{"class":1178},[826,6534,6535],{"class":1047,"line":1789},[826,6536,1547],{"emptyLinePlaceholder":877},[826,6538,6539,6541,6543,6545,6547,6550,6552,6555,6557,6560],{"class":1047,"line":1821},[826,6540,5398],{"class":1499},[826,6542,1895],{"class":1178},[826,6544,6470],{"class":1051},[826,6546,1282],{"class":1178},[826,6548,6549],{"class":1285},"compare_digest",[826,6551,1289],{"class":1178},[826,6553,6554],{"class":1285},"expected",[826,6556,1299],{"class":1178},[826,6558,6559],{"class":1285}," received_signature",[826,6561,4230],{"class":1178},[826,6563,6564,6567,6570,6573,6575,6577,6579,6581,6583,6586,6588,6591,6594,6596,6599],{"class":1047,"line":1853},[826,6565,6566],{"class":1499},"                return",[826,6568,6569],{"class":1285}," Response",[826,6571,6572],{"class":1178},"({",[826,6574,1292],{"class":1178},[826,6576,5460],{"class":1182},[826,6578,1292],{"class":1178},[826,6580,2182],{"class":1178},[826,6582,1557],{"class":1178},[826,6584,6585],{"class":1182},"invalid_signature",[826,6587,1292],{"class":1178},[826,6589,6590],{"class":1178},"},",[826,6592,6593],{"class":1302}," status",[826,6595,1179],{"class":1178},[826,6597,6598],{"class":1573},"200",[826,6600,1314],{"class":1178},[826,6602,6603],{"class":1047,"line":1885},[826,6604,1547],{"emptyLinePlaceholder":877},[826,6606,6607],{"class":1047,"line":1890},[826,6608,6609],{"class":1169},"        # Parse event\n",[826,6611,6612,6615,6617,6619,6621,6624,6626,6628,6630,6632,6634,6636,6638,6640],{"class":1047,"line":1937},[826,6613,6614],{"class":1051},"        event ",[826,6616,1179],{"class":1178},[826,6618,6385],{"class":1051},[826,6620,1282],{"class":1178},[826,6622,6623],{"class":1732},"data",[826,6625,1282],{"class":1178},[826,6627,2051],{"class":1285},[826,6629,1289],{"class":1178},[826,6631,1292],{"class":1178},[826,6633,3823],{"class":1182},[826,6635,1292],{"class":1178},[826,6637,1299],{"class":1178},[826,6639,1784],{"class":1178},[826,6641,1314],{"class":1178},[826,6643,6644,6647,6649,6651,6653,6655,6657,6659,6661,6663,6666,6668,6670],{"class":1047,"line":1958},[826,6645,6646],{"class":1051},"        meta ",[826,6648,1179],{"class":1178},[826,6650,6385],{"class":1051},[826,6652,1282],{"class":1178},[826,6654,6623],{"class":1732},[826,6656,1282],{"class":1178},[826,6658,2051],{"class":1285},[826,6660,1289],{"class":1178},[826,6662,1292],{"class":1178},[826,6664,6665],{"class":1182},"meta",[826,6667,1292],{"class":1178},[826,6669,1299],{"class":1178},[826,6671,6672],{"class":1178}," {})\n",[826,6674,6675,6678,6680,6683,6685,6687,6689,6691,6693,6695,6697,6699],{"class":1047,"line":1965},[826,6676,6677],{"class":1051},"        document_id ",[826,6679,1179],{"class":1178},[826,6681,6682],{"class":1051}," meta",[826,6684,1282],{"class":1178},[826,6686,2051],{"class":1285},[826,6688,1289],{"class":1178},[826,6690,1292],{"class":1178},[826,6692,2971],{"class":1182},[826,6694,1292],{"class":1178},[826,6696,1299],{"class":1178},[826,6698,1784],{"class":1178},[826,6700,1314],{"class":1178},[826,6702,6703],{"class":1047,"line":1970},[826,6704,1547],{"emptyLinePlaceholder":877},[826,6706,6707,6709,6712,6714,6716,6718,6720,6722,6724,6726,6729,6731],{"class":1047,"line":1983},[826,6708,1724],{"class":1499},[826,6710,6711],{"class":1051}," event ",[826,6713,5667],{"class":1178},[826,6715,2732],{"class":1178},[826,6717,1292],{"class":1178},[826,6719,6013],{"class":1182},[826,6721,1292],{"class":1178},[826,6723,1299],{"class":1178},[826,6725,1557],{"class":1178},[826,6727,6728],{"class":1182},"document.update",[826,6730,1292],{"class":1178},[826,6732,4230],{"class":1178},[826,6734,6735,6738,6740,6742,6744,6746,6748,6750],{"class":1047,"line":1991},[826,6736,6737],{"class":1051},"            signable ",[826,6739,1179],{"class":1178},[826,6741,4217],{"class":1051},[826,6743,1282],{"class":1178},[826,6745,5216],{"class":1732},[826,6747,1282],{"class":1178},[826,6749,2051],{"class":1285},[826,6751,2133],{"class":1178},[826,6753,6754,6757,6759,6761],{"class":1047,"line":1996},[826,6755,6756],{"class":1302},"                signnow_document_id",[826,6758,1179],{"class":1178},[826,6760,2971],{"class":1285},[826,6762,2159],{"class":1178},[826,6764,6765,6768,6770,6772,6774,6776,6778,6780,6782,6784],{"class":1047,"line":2003},[826,6766,6767],{"class":1302},"                status__in",[826,6769,5511],{"class":1178},[826,6771,1292],{"class":1178},[826,6773,4297],{"class":1182},[826,6775,1292],{"class":1178},[826,6777,1299],{"class":1178},[826,6779,1557],{"class":1178},[826,6781,4320],{"class":1182},[826,6783,1292],{"class":1178},[826,6785,3352],{"class":1178},[826,6787,6788],{"class":1047,"line":2028},[826,6789,2329],{"class":1178},[826,6791,6792,6794,6796,6798,6800,6802,6804,6806,6808],{"class":1047,"line":2038},[826,6793,5433],{"class":1051},[826,6795,1282],{"class":1178},[826,6797,5460],{"class":1732},[826,6799,1763],{"class":1178},[826,6801,4217],{"class":1051},[826,6803,1282],{"class":1178},[826,6805,4658],{"class":1732},[826,6807,1282],{"class":1178},[826,6809,6810],{"class":1732},"SIGNED\n",[826,6812,6813,6815,6817,6820,6822,6824,6826,6828],{"class":1047,"line":2061},[826,6814,5433],{"class":1051},[826,6816,1282],{"class":1178},[826,6818,6819],{"class":1732},"signed_at",[826,6821,1763],{"class":1178},[826,6823,5487],{"class":1051},[826,6825,1282],{"class":1178},[826,6827,5492],{"class":1285},[826,6829,2343],{"class":1178},[826,6831,6832,6834,6836,6838,6840,6842,6844,6846,6848,6850,6852,6854,6856,6858],{"class":1047,"line":2071},[826,6833,5433],{"class":1051},[826,6835,1282],{"class":1178},[826,6837,5503],{"class":1285},[826,6839,1289],{"class":1178},[826,6841,5508],{"class":1302},[826,6843,5511],{"class":1178},[826,6845,1292],{"class":1178},[826,6847,5460],{"class":1182},[826,6849,1292],{"class":1178},[826,6851,1299],{"class":1178},[826,6853,1557],{"class":1178},[826,6855,6819],{"class":1182},[826,6857,1292],{"class":1178},[826,6859,5536],{"class":1178},[826,6861,6862],{"class":1047,"line":2079},[826,6863,1547],{"emptyLinePlaceholder":877},[826,6865,6866],{"class":1047,"line":2084},[826,6867,6868],{"class":1169},"            # Download the signed PDF asynchronously\n",[826,6870,6871,6874,6876,6879,6881,6883,6885,6887],{"class":1047,"line":2118},[826,6872,6873],{"class":1051},"            download_signed_pdf",[826,6875,1282],{"class":1178},[826,6877,6878],{"class":1285},"delay",[826,6880,1289],{"class":1178},[826,6882,5609],{"class":1285},[826,6884,1282],{"class":1178},[826,6886,2779],{"class":1732},[826,6888,1314],{"class":1178},[826,6890,6891],{"class":1047,"line":2136},[826,6892,1547],{"emptyLinePlaceholder":877},[826,6894,6895],{"class":1047,"line":2162},[826,6896,6897],{"class":1169},"        # Always return 200 to acknowledge receipt\n",[826,6899,6900,6902,6904,6906,6908,6910,6912,6914,6916,6919,6921,6923,6925,6927,6929],{"class":1047,"line":2171},[826,6901,1986],{"class":1499},[826,6903,6569],{"class":1285},[826,6905,6572],{"class":1178},[826,6907,1292],{"class":1178},[826,6909,5460],{"class":1182},[826,6911,1292],{"class":1178},[826,6913,2182],{"class":1178},[826,6915,1557],{"class":1178},[826,6917,6918],{"class":1182},"ok",[826,6920,1292],{"class":1178},[826,6922,6590],{"class":1178},[826,6924,6593],{"class":1302},[826,6926,1179],{"class":1178},[826,6928,6598],{"class":1573},[826,6930,1314],{"class":1178},[1239,6932,6933],{},[746,6934,6935],{},"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.",[746,6937,6938],{},"Wire it up in your URL configuration:",[1037,6940,6943],{"className":1259,"code":6941,"filename":6942,"language":1262,"meta":865,"style":865},"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",[1043,6944,6945,6961,6981,6985,6995,7035],{"__ignoreMap":865},[826,6946,6947,6949,6951,6953,6956,6958],{"class":1047,"line":1048},[826,6948,1508],{"class":1499},[826,6950,1511],{"class":1051},[826,6952,1282],{"class":1178},[826,6954,6955],{"class":1051},"urls ",[826,6957,1500],{"class":1499},[826,6959,6960],{"class":1051}," path\n",[826,6962,6963,6965,6967,6969,6972,6974,6976,6978],{"class":1047,"line":866},[826,6964,1508],{"class":1499},[826,6966,5168],{"class":1051},[826,6968,1282],{"class":1178},[826,6970,6971],{"class":1051},"api",[826,6973,1282],{"class":1178},[826,6975,6300],{"class":1051},[826,6977,1500],{"class":1499},[826,6979,6980],{"class":1051}," SignNowWebhookView\n",[826,6982,6983],{"class":1047,"line":1060},[826,6984,1547],{"emptyLinePlaceholder":877},[826,6986,6987,6990,6992],{"class":1047,"line":1066},[826,6988,6989],{"class":1051},"urlpatterns ",[826,6991,1179],{"class":1178},[826,6993,6994],{"class":1178}," [\n",[826,6996,6997,7000,7002,7004,7007,7009,7011,7013,7015,7018,7021,7024,7026,7028,7031,7033],{"class":1047,"line":1072},[826,6998,6999],{"class":1285},"    path",[826,7001,1289],{"class":1178},[826,7003,1292],{"class":1178},[826,7005,7006],{"class":1182},"webhooks/",[826,7008,1292],{"class":1178},[826,7010,1299],{"class":1178},[826,7012,6320],{"class":1285},[826,7014,1282],{"class":1178},[826,7016,7017],{"class":1285},"as_view",[826,7019,7020],{"class":1178},"(),",[826,7022,7023],{"class":1302}," name",[826,7025,1179],{"class":1178},[826,7027,1292],{"class":1178},[826,7029,7030],{"class":1182},"signnow-webhook",[826,7032,1292],{"class":1178},[826,7034,2712],{"class":1178},[826,7036,7037],{"class":1047,"line":1218},[826,7038,2390],{"class":1178},[746,7040,7041],{},"And include it in your main API URLs:",[1037,7043,7046],{"className":1259,"code":7044,"filename":7045,"language":1262,"meta":865,"style":865},"urlpatterns = [\n    # ... other routes\n    path(\"v1/signnow/\", include(\"signnow.api.urls\")),\n]\n","apps/api/urls.py",[1043,7047,7048,7056,7061,7091],{"__ignoreMap":865},[826,7049,7050,7052,7054],{"class":1047,"line":1048},[826,7051,6989],{"class":1051},[826,7053,1179],{"class":1178},[826,7055,6994],{"class":1178},[826,7057,7058],{"class":1047,"line":866},[826,7059,7060],{"class":1169},"    # ... other routes\n",[826,7062,7063,7065,7067,7069,7072,7074,7076,7079,7081,7083,7086,7088],{"class":1047,"line":1060},[826,7064,6999],{"class":1285},[826,7066,1289],{"class":1178},[826,7068,1292],{"class":1178},[826,7070,7071],{"class":1182},"v1/signnow/",[826,7073,1292],{"class":1178},[826,7075,1299],{"class":1178},[826,7077,7078],{"class":1285}," include",[826,7080,1289],{"class":1178},[826,7082,1292],{"class":1178},[826,7084,7085],{"class":1182},"signnow.api.urls",[826,7087,1292],{"class":1178},[826,7089,7090],{"class":1178},")),\n",[826,7092,7093],{"class":1047,"line":1066},[826,7094,2390],{"class":1178},[753,7096,7098],{"id":7097},"step-6-download-the-signed-pdf","Step 6: Download the signed PDF",[746,7100,7101],{},"The second Celery task fetches the completed, signed document from SignNow and saves it locally:",[1037,7103,7105],{"className":1259,"code":7104,"filename":4895,"language":1262,"meta":865,"style":865},"@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",[1043,7106,7107,7137,7158,7167,7181,7199,7203,7230,7234,7239,7264,7269,7273,7297,7307,7325,7329,7373,7409,7430,7449],{"__ignoreMap":865},[826,7108,7109,7111,7113,7115,7117,7119,7121,7123,7126,7128,7130,7132,7135],{"class":1047,"line":1048},[826,7110,2534],{"class":1178},[826,7112,5099],{"class":1285},[826,7114,1289],{"class":1178},[826,7116,5104],{"class":1302},[826,7118,4531],{"class":1178},[826,7120,5109],{"class":1302},[826,7122,1179],{"class":1178},[826,7124,7125],{"class":1573},"5",[826,7127,1299],{"class":1178},[826,7129,5119],{"class":1302},[826,7131,1179],{"class":1178},[826,7133,7134],{"class":1573},"120",[826,7136,1314],{"class":1178},[826,7138,7139,7141,7144,7146,7148,7150,7152,7154,7156],{"class":1047,"line":866},[826,7140,2541],{"class":1592},[826,7142,7143],{"class":1285}," download_signed_pdf",[826,7145,1289],{"class":1178},[826,7147,5139],{"class":5138},[826,7149,1299],{"class":1178},[826,7151,5144],{"class":1302},[826,7153,2182],{"class":1178},[826,7155,5149],{"class":1596},[826,7157,4230],{"class":1178},[826,7159,7160,7162,7165],{"class":1047,"line":1060},[826,7161,1606],{"class":1499},[826,7163,7164],{"class":1169},"Download the signed PDF from SignNow and save it locally.",[826,7166,1612],{"class":1499},[826,7168,7169,7171,7173,7175,7177,7179],{"class":1047,"line":1066},[826,7170,5165],{"class":1499},[826,7172,5168],{"class":1051},[826,7174,1282],{"class":1178},[826,7176,4181],{"class":1051},[826,7178,1500],{"class":1499},[826,7180,5177],{"class":1051},[826,7182,7183,7185,7187,7189,7191,7193,7195,7197],{"class":1047,"line":1072},[826,7184,5165],{"class":1499},[826,7186,5168],{"class":1051},[826,7188,1282],{"class":1178},[826,7190,5188],{"class":1051},[826,7192,1282],{"class":1178},[826,7194,5193],{"class":1051},[826,7196,1500],{"class":1499},[826,7198,5198],{"class":1051},[826,7200,7201],{"class":1047,"line":1218},[826,7202,1547],{"emptyLinePlaceholder":877},[826,7204,7205,7207,7209,7211,7213,7215,7217,7219,7221,7223,7225,7228],{"class":1047,"line":1229},[826,7206,5207],{"class":1051},[826,7208,1179],{"class":1178},[826,7210,4217],{"class":1051},[826,7212,1282],{"class":1178},[826,7214,5216],{"class":1732},[826,7216,1282],{"class":1178},[826,7218,2051],{"class":1285},[826,7220,1289],{"class":1178},[826,7222,2779],{"class":1302},[826,7224,1179],{"class":1178},[826,7226,7227],{"class":1285},"signable_document_id",[826,7229,1314],{"class":1178},[826,7231,7232],{"class":1047,"line":1584},[826,7233,1547],{"emptyLinePlaceholder":877},[826,7235,7236],{"class":1047,"line":1589},[826,7237,7238],{"class":1169},"    # Skip if already downloaded (idempotent)\n",[826,7240,7241,7243,7245,7247,7249,7251,7253,7255,7257,7259,7262],{"class":1047,"line":1603},[826,7242,2608],{"class":1499},[826,7244,5277],{"class":1051},[826,7246,1282],{"class":1178},[826,7248,5460],{"class":1732},[826,7250,5693],{"class":1178},[826,7252,4217],{"class":1051},[826,7254,1282],{"class":1178},[826,7256,4658],{"class":1732},[826,7258,1282],{"class":1178},[826,7260,7261],{"class":1732},"DOWNLOADED",[826,7263,1600],{"class":1178},[826,7265,7266],{"class":1047,"line":1615},[826,7267,7268],{"class":1499},"        return\n",[826,7270,7271],{"class":1047,"line":1620},[826,7272,1547],{"emptyLinePlaceholder":877},[826,7274,7275,7278,7280,7282,7284,7287,7289,7291,7293,7295],{"class":1047,"line":1631},[826,7276,7277],{"class":1051},"    pdf_bytes ",[826,7279,1179],{"class":1178},[826,7281,1597],{"class":1051},[826,7283,1282],{"class":1178},[826,7285,7286],{"class":1285},"download_signed_document",[826,7288,1289],{"class":1178},[826,7290,5609],{"class":1285},[826,7292,1282],{"class":1178},[826,7294,5366],{"class":1732},[826,7296,1314],{"class":1178},[826,7298,7299,7301,7303,7305],{"class":1047,"line":1642},[826,7300,2608],{"class":1499},[826,7302,1895],{"class":1178},[826,7304,2553],{"class":1051},[826,7306,1600],{"class":1178},[826,7308,7309,7312,7314,7316,7318,7321,7323],{"class":1047,"line":1652},[826,7310,7311],{"class":1499},"        raise",[826,7313,5413],{"class":1596},[826,7315,1289],{"class":1178},[826,7317,1292],{"class":1178},[826,7319,7320],{"class":1182},"Failed to download signed document",[826,7322,1292],{"class":1178},[826,7324,1314],{"class":1178},[826,7326,7327],{"class":1047,"line":1662},[826,7328,1547],{"emptyLinePlaceholder":877},[826,7330,7331,7334,7336,7338,7341,7343,7345,7347,7350,7352,7354,7356,7359,7361,7363,7365,7367,7369,7371],{"class":1047,"line":1672},[826,7332,7333],{"class":1051},"    signed_filename ",[826,7335,1179],{"class":1178},[826,7337,2280],{"class":1592},[826,7339,7340],{"class":1182},"\"signed-",[826,7342,2144],{"class":1573},[826,7344,5609],{"class":1051},[826,7346,1282],{"class":1178},[826,7348,7349],{"class":1732},"original_pdf_name",[826,7351,1282],{"class":1178},[826,7353,5327],{"class":1285},[826,7355,1289],{"class":1178},[826,7357,7358],{"class":1178},"'",[826,7360,5334],{"class":1182},[826,7362,7358],{"class":1178},[826,7364,5339],{"class":1178},[826,7366,5342],{"class":1573},[826,7368,3521],{"class":1178},[826,7370,2153],{"class":1573},[826,7372,1563],{"class":1182},[826,7374,7375,7378,7380,7383,7385,7387,7389,7392,7394,7397,7399,7401,7403,7406],{"class":1047,"line":1677},[826,7376,7377],{"class":1051},"    signable",[826,7379,1282],{"class":1178},[826,7381,7382],{"class":1732},"signed_pdf",[826,7384,1282],{"class":1178},[826,7386,5503],{"class":1285},[826,7388,1289],{"class":1178},[826,7390,7391],{"class":1285},"signed_filename",[826,7393,1299],{"class":1178},[826,7395,7396],{"class":1285}," ContentFile",[826,7398,1289],{"class":1178},[826,7400,5387],{"class":1285},[826,7402,2997],{"class":1178},[826,7404,7405],{"class":1302}," save",[826,7407,7408],{"class":1178},"=False)\n",[826,7410,7411,7413,7415,7417,7419,7421,7423,7425,7427],{"class":1047,"line":1686},[826,7412,7377],{"class":1051},[826,7414,1282],{"class":1178},[826,7416,5460],{"class":1732},[826,7418,1763],{"class":1178},[826,7420,4217],{"class":1051},[826,7422,1282],{"class":1178},[826,7424,4658],{"class":1732},[826,7426,1282],{"class":1178},[826,7428,7429],{"class":1732},"DOWNLOADED\n",[826,7431,7432,7434,7436,7439,7441,7443,7445,7447],{"class":1047,"line":1710},[826,7433,7377],{"class":1051},[826,7435,1282],{"class":1178},[826,7437,7438],{"class":1732},"downloaded_at",[826,7440,1763],{"class":1178},[826,7442,5487],{"class":1051},[826,7444,1282],{"class":1178},[826,7446,5492],{"class":1285},[826,7448,2343],{"class":1178},[826,7450,7451,7453,7455,7457,7459,7461,7463,7465,7467,7469,7471,7473,7475,7477,7479,7481,7483,7485],{"class":1047,"line":1721},[826,7452,7377],{"class":1051},[826,7454,1282],{"class":1178},[826,7456,5503],{"class":1285},[826,7458,1289],{"class":1178},[826,7460,5508],{"class":1302},[826,7462,5511],{"class":1178},[826,7464,1292],{"class":1178},[826,7466,7382],{"class":1182},[826,7468,1292],{"class":1178},[826,7470,1299],{"class":1178},[826,7472,1557],{"class":1178},[826,7474,5460],{"class":1182},[826,7476,1292],{"class":1178},[826,7478,1299],{"class":1178},[826,7480,1557],{"class":1178},[826,7482,7438],{"class":1182},[826,7484,1292],{"class":1178},[826,7486,5536],{"class":1178},[753,7488,7490],{"id":7489},"step-7-admin-integration","Step 7: Admin integration",[746,7492,7493],{},"Finally, we added a read-only admin panel with color-coded status badges so our team can monitor signing progress at a glance:",[1037,7495,7498],{"className":1259,"code":7496,"filename":7497,"language":1262,"meta":865,"style":865},"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",[1043,7499,7500,7516,7537,7541,7545,7563,7581,7633,7658,7691,7695,7721,7739,7748,7767,7786,7805,7824,7843,7862,7867,7900,7909,7926,7942,7949,7962],{"__ignoreMap":865},[826,7501,7502,7504,7506,7508,7511,7513],{"class":1047,"line":1048},[826,7503,1508],{"class":1499},[826,7505,1511],{"class":1051},[826,7507,1282],{"class":1178},[826,7509,7510],{"class":1051},"contrib ",[826,7512,1500],{"class":1499},[826,7514,7515],{"class":1051}," admin\n",[826,7517,7518,7520,7522,7524,7527,7529,7532,7534],{"class":1047,"line":866},[826,7519,1508],{"class":1499},[826,7521,1511],{"class":1051},[826,7523,1282],{"class":1178},[826,7525,7526],{"class":1051},"utils",[826,7528,1282],{"class":1178},[826,7530,7531],{"class":1051},"html ",[826,7533,1500],{"class":1499},[826,7535,7536],{"class":1051}," format_html\n",[826,7538,7539],{"class":1047,"line":1060},[826,7540,1547],{"emptyLinePlaceholder":877},[826,7542,7543],{"class":1047,"line":1066},[826,7544,1547],{"emptyLinePlaceholder":877},[826,7546,7547,7549,7552,7554,7557,7559,7561],{"class":1047,"line":1072},[826,7548,2534],{"class":1178},[826,7550,7551],{"class":1285},"admin",[826,7553,1282],{"class":1178},[826,7555,7556],{"class":1285},"register",[826,7558,1289],{"class":1178},[826,7560,1096],{"class":1285},[826,7562,1314],{"class":1178},[826,7564,7565,7567,7570,7572,7574,7576,7579],{"class":1047,"line":1218},[826,7566,1593],{"class":1592},[826,7568,7569],{"class":1596}," SignableDocumentAdmin",[826,7571,1289],{"class":1178},[826,7573,7551],{"class":1596},[826,7575,1282],{"class":1178},[826,7577,7578],{"class":1596},"ModelAdmin",[826,7580,4230],{"class":1178},[826,7582,7583,7586,7588,7590,7592,7595,7597,7599,7601,7603,7605,7607,7609,7612,7614,7616,7618,7621,7623,7625,7627,7629,7631],{"class":1047,"line":1229},[826,7584,7585],{"class":1051},"    list_display ",[826,7587,1179],{"class":1178},[826,7589,6350],{"class":1178},[826,7591,1292],{"class":1178},[826,7593,7594],{"class":1182},"signer_name",[826,7596,1292],{"class":1178},[826,7598,1299],{"class":1178},[826,7600,1557],{"class":1178},[826,7602,5738],{"class":1182},[826,7604,1292],{"class":1178},[826,7606,1299],{"class":1178},[826,7608,1557],{"class":1178},[826,7610,7611],{"class":1182},"status_badge",[826,7613,1292],{"class":1178},[826,7615,1299],{"class":1178},[826,7617,1557],{"class":1178},[826,7619,7620],{"class":1182},"created_at",[826,7622,1292],{"class":1178},[826,7624,1299],{"class":1178},[826,7626,1557],{"class":1178},[826,7628,6819],{"class":1182},[826,7630,1292],{"class":1178},[826,7632,2390],{"class":1178},[826,7634,7635,7638,7640,7642,7644,7646,7648,7650,7652,7654,7656],{"class":1047,"line":1584},[826,7636,7637],{"class":1051},"    list_filter ",[826,7639,1179],{"class":1178},[826,7641,6350],{"class":1178},[826,7643,1292],{"class":1178},[826,7645,5460],{"class":1182},[826,7647,1292],{"class":1178},[826,7649,1299],{"class":1178},[826,7651,1557],{"class":1178},[826,7653,7620],{"class":1182},[826,7655,1292],{"class":1178},[826,7657,2390],{"class":1178},[826,7659,7660,7663,7665,7667,7669,7671,7673,7675,7677,7679,7681,7683,7685,7687,7689],{"class":1047,"line":1589},[826,7661,7662],{"class":1051},"    search_fields ",[826,7664,1179],{"class":1178},[826,7666,6350],{"class":1178},[826,7668,1292],{"class":1178},[826,7670,5738],{"class":1182},[826,7672,1292],{"class":1178},[826,7674,1299],{"class":1178},[826,7676,1557],{"class":1178},[826,7678,7594],{"class":1182},[826,7680,1292],{"class":1178},[826,7682,1299],{"class":1178},[826,7684,1557],{"class":1178},[826,7686,5366],{"class":1182},[826,7688,1292],{"class":1178},[826,7690,2390],{"class":1178},[826,7692,7693],{"class":1047,"line":1603},[826,7694,1547],{"emptyLinePlaceholder":877},[826,7696,7697,7699,7701,7703,7706,7708,7711,7713,7715,7717,7719],{"class":1047,"line":1615},[826,7698,1680],{"class":1178},[826,7700,7551],{"class":1285},[826,7702,1282],{"class":1178},[826,7704,7705],{"class":1285},"display",[826,7707,1289],{"class":1178},[826,7709,7710],{"class":1302},"description",[826,7712,1179],{"class":1178},[826,7714,1292],{"class":1178},[826,7716,4658],{"class":1182},[826,7718,1292],{"class":1178},[826,7720,1314],{"class":1178},[826,7722,7723,7725,7728,7730,7732,7734,7737],{"class":1047,"line":1620},[826,7724,1689],{"class":1592},[826,7726,7727],{"class":1285}," status_badge",[826,7729,1289],{"class":1178},[826,7731,5139],{"class":5138},[826,7733,1299],{"class":1178},[826,7735,7736],{"class":1302}," obj",[826,7738,4230],{"class":1178},[826,7740,7741,7744,7746],{"class":1047,"line":1631},[826,7742,7743],{"class":1051},"        colors ",[826,7745,1179],{"class":1178},[826,7747,3815],{"class":1178},[826,7749,7750,7752,7754,7756,7758,7760,7763,7765],{"class":1047,"line":1642},[826,7751,3884],{"class":1178},[826,7753,4274],{"class":1182},[826,7755,1292],{"class":1178},[826,7757,2182],{"class":1178},[826,7759,1557],{"class":1178},[826,7761,7762],{"class":1182},"#6b7280",[826,7764,1292],{"class":1178},[826,7766,2159],{"class":1178},[826,7768,7769,7771,7773,7775,7777,7779,7782,7784],{"class":1047,"line":1652},[826,7770,3884],{"class":1178},[826,7772,4297],{"class":1182},[826,7774,1292],{"class":1178},[826,7776,2182],{"class":1178},[826,7778,1557],{"class":1178},[826,7780,7781],{"class":1182},"#3b82f6",[826,7783,1292],{"class":1178},[826,7785,2159],{"class":1178},[826,7787,7788,7790,7792,7794,7796,7798,7801,7803],{"class":1047,"line":1662},[826,7789,3884],{"class":1178},[826,7791,4320],{"class":1182},[826,7793,1292],{"class":1178},[826,7795,2182],{"class":1178},[826,7797,1557],{"class":1178},[826,7799,7800],{"class":1182},"#f59e0b",[826,7802,1292],{"class":1178},[826,7804,2159],{"class":1178},[826,7806,7807,7809,7811,7813,7815,7817,7820,7822],{"class":1047,"line":1672},[826,7808,3884],{"class":1178},[826,7810,4343],{"class":1182},[826,7812,1292],{"class":1178},[826,7814,2182],{"class":1178},[826,7816,1557],{"class":1178},[826,7818,7819],{"class":1182},"#22c55e",[826,7821,1292],{"class":1178},[826,7823,2159],{"class":1178},[826,7825,7826,7828,7830,7832,7834,7836,7839,7841],{"class":1047,"line":1677},[826,7827,3884],{"class":1178},[826,7829,4366],{"class":1182},[826,7831,1292],{"class":1178},[826,7833,2182],{"class":1178},[826,7835,1557],{"class":1178},[826,7837,7838],{"class":1182},"#059669",[826,7840,1292],{"class":1178},[826,7842,2159],{"class":1178},[826,7844,7845,7847,7849,7851,7853,7855,7858,7860],{"class":1047,"line":1686},[826,7846,3884],{"class":1178},[826,7848,4389],{"class":1182},[826,7850,1292],{"class":1178},[826,7852,2182],{"class":1178},[826,7854,1557],{"class":1178},[826,7856,7857],{"class":1182},"#ef4444",[826,7859,1292],{"class":1178},[826,7861,2159],{"class":1178},[826,7863,7864],{"class":1047,"line":1710},[826,7865,7866],{"class":1178},"        }\n",[826,7868,7869,7872,7874,7877,7879,7881,7883,7886,7888,7890,7892,7894,7896,7898],{"class":1047,"line":1721},[826,7870,7871],{"class":1051},"        color ",[826,7873,1179],{"class":1178},[826,7875,7876],{"class":1051}," colors",[826,7878,1282],{"class":1178},[826,7880,2051],{"class":1285},[826,7882,1289],{"class":1178},[826,7884,7885],{"class":1285},"obj",[826,7887,1282],{"class":1178},[826,7889,5460],{"class":1732},[826,7891,1299],{"class":1178},[826,7893,1557],{"class":1178},[826,7895,7762],{"class":1182},[826,7897,1292],{"class":1178},[826,7899,1314],{"class":1178},[826,7901,7902,7904,7907],{"class":1047,"line":1738},[826,7903,1986],{"class":1499},[826,7905,7906],{"class":1285}," format_html",[826,7908,2133],{"class":1178},[826,7910,7911,7914,7917,7920,7923],{"class":1047,"line":1747},[826,7912,7913],{"class":1178},"            '",[826,7915,7916],{"class":1182},"\u003Cspan style=\"background-color: ",[826,7918,7919],{"class":1573},"{}",[826,7921,7922],{"class":1182},"; color: white; padding: 2px 8px; ",[826,7924,7925],{"class":1178},"'\n",[826,7927,7928,7930,7933,7935,7938,7940],{"class":1047,"line":1752},[826,7929,7913],{"class":1178},[826,7931,7932],{"class":1182},"border-radius: 4px; font-size: 12px;\">",[826,7934,7919],{"class":1573},[826,7936,7937],{"class":1182},"\u003C/span>",[826,7939,7358],{"class":1178},[826,7941,2159],{"class":1178},[826,7943,7944,7947],{"class":1047,"line":1789},[826,7945,7946],{"class":1285},"            color",[826,7948,2159],{"class":1178},[826,7950,7951,7954,7956,7959],{"class":1047,"line":1821},[826,7952,7953],{"class":1285},"            obj",[826,7955,1282],{"class":1178},[826,7957,7958],{"class":1285},"get_status_display",[826,7960,7961],{"class":1178},"(),\n",[826,7963,7964],{"class":1047,"line":1853},[826,7965,2756],{"class":1178},[753,7967,7969],{"id":7968},"lessons-learned","Lessons learned",[746,7971,7972],{},"After running this in production, here are the things we wish we'd known earlier:",[1482,7974,7976],{"id":7975},"_1-role-based-invites-require-a-two-step-process","1. Role-based invites require a two-step process",[746,7978,7979,7980,7983,7984,7986],{},"You can't send a role-based invite immediately after adding fields. You need to ",[970,7981,7982],{},"re-fetch the document"," to get the ",[1043,7985,5765],{}," that SignNow assigns when processing the fields. This caught us off guard because freeform invites don't have this requirement.",[1482,7988,7990],{"id":7989},"_2-token-caching-is-essential","2. Token caching is essential",[746,7992,7993],{},"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.",[1482,7995,7997],{"id":7996},"_3-webhook-signatures-use-hmac-sha256","3. Webhook signatures use HMAC-SHA256",[746,7999,8000,8001,8003,8004,8007],{},"Always verify webhook payloads in production. SignNow sends a ",[1043,8002,6452],{}," header containing an HMAC-SHA256 digest of the request body. Use ",[1043,8005,8006],{},"hmac.compare_digest()"," for timing-safe comparison.",[1482,8009,8011],{"id":8010},"_4-custom-invite-messages-require-a-paid-plan","4. Custom invite messages require a paid plan",[746,8013,8014],{},"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.",[1482,8016,8018],{"id":8017},"_5-signature-field-coordinates-need-calibration","5. Signature field coordinates need calibration",[746,8020,1243,8021,6214,8023,6214,8025,8027,8028,8030],{},[1043,8022,4935],{},[1043,8024,4951],{},[1043,8026,4967],{},", and ",[1043,8029,4983],{}," 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.",[753,8032,8034],{"id":8033},"summary","Summary",[746,8036,8037],{},"Here's the complete API flow at a glance:",[946,8039,8040,8056],{},[949,8041,8042],{},[952,8043,8044,8047,8050,8053],{},[955,8045,8046],{},"Step",[955,8048,8049],{},"API Endpoint",[955,8051,8052],{},"Method",[955,8054,8055],{},"Purpose",[962,8057,8058,8073,8088,8103,8118,8132,8147],{},[952,8059,8060,8062,8067,8070],{},[967,8061,5342],{},[967,8063,8064],{},[1043,8065,8066],{},"/oauth2/token",[967,8068,8069],{},"POST",[967,8071,8072],{},"Get access token",[952,8074,8075,8078,8083,8085],{},[967,8076,8077],{},"2",[967,8079,8080],{},[1043,8081,8082],{},"/document",[967,8084,8069],{},[967,8086,8087],{},"Upload PDF",[952,8089,8090,8092,8097,8100],{},[967,8091,5114],{},[967,8093,8094],{},[1043,8095,8096],{},"/document/{id}",[967,8098,8099],{},"PUT",[967,8101,8102],{},"Add signature fields",[952,8104,8105,8108,8112,8115],{},[967,8106,8107],{},"4",[967,8109,8110],{},[1043,8111,8096],{},[967,8113,8114],{},"GET",[967,8116,8117],{},"Retrieve role IDs",[952,8119,8120,8122,8127,8129],{},[967,8121,7125],{},[967,8123,8124],{},[1043,8125,8126],{},"/document/{id}/invite",[967,8128,8069],{},[967,8130,8131],{},"Send signing invite",[952,8133,8134,8137,8142,8144],{},[967,8135,8136],{},"6",[967,8138,8139],{},[1043,8140,8141],{},"/v2/events",[967,8143,8069],{},[967,8145,8146],{},"Register webhook",[952,8148,8149,8152,8157,8159],{},[967,8150,8151],{},"7",[967,8153,8154],{},[1043,8155,8156],{},"/document/{id}/download",[967,8158,8114],{},[967,8160,8161],{},"Download signed PDF",[746,8163,8164,8165,8168],{},"The full integration is about ",[970,8166,8167],{},"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.",[8170,8171,8172],"style",{},"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":865,"searchDepth":866,"depth":866,"links":8174},[8175,8176,8177,8178,8184,8185,8186,8187,8188,8189,8196],{"id":940,"depth":866,"text":941},{"id":1031,"depth":866,"text":1032},{"id":1119,"depth":866,"text":1120},{"id":1473,"depth":866,"text":1474,"children":8179},[8180,8181,8182,8183],{"id":1484,"depth":1060,"text":1485},{"id":2520,"depth":1060,"text":2521},{"id":3277,"depth":1060,"text":3278},{"id":3702,"depth":1060,"text":3703},{"id":4119,"depth":866,"text":4120},{"id":4887,"depth":866,"text":4888},{"id":6228,"depth":866,"text":6229},{"id":7097,"depth":866,"text":7098},{"id":7489,"depth":866,"text":7490},{"id":7968,"depth":866,"text":7969,"children":8190},[8191,8192,8193,8194,8195],{"id":7975,"depth":1060,"text":7976},{"id":7989,"depth":1060,"text":7990},{"id":7996,"depth":1060,"text":7997},{"id":8010,"depth":1060,"text":8011},{"id":8017,"depth":1060,"text":8018},{"id":8033,"depth":866,"text":8034},"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":8200,"credit":8201},"/images/blog/musictechlab_blog_signnow_django_integration.webp","Photo by [Scott Graham](https://unsplash.com/@amstram) on [Unsplash](https://unsplash.com/photos/OQMZwNd3ThU)",{"enabled":877,"items":8203},[8204,8206,8209,8212],{"text":8205,"icon":884},"The full SignNow Django integration is about 400 lines across four files.",{"text":8207,"icon":8208},"All SignNow API calls run in Celery tasks to keep the request cycle fast.","i-lucide-zap",{"text":8210,"icon":8211},"Role-based invites need a two-step process: add fields, then re-fetch to get role IDs.","i-lucide-git-branch",{"text":8213,"icon":8214},"Cache OAuth2 tokens in Redis with a 5-minute buffer to avoid extra round-trips.","i-lucide-clock",{},{"title":558,"description":8198},[894],"B2omIw3TRWUTJSZukF0UtP4lbpEdNuc7Vc86WdkLOqQ",{"id":8220,"title":422,"authors":8221,"badge":8224,"body":8227,"category":873,"client":741,"date":10815,"description":10816,"extension":876,"faq":741,"featured":877,"featuredOrder":1631,"hidden":69,"image":10817,"keyTakeaways":10819,"meta":10833,"navigation":877,"path":423,"seo":10834,"status":741,"stem":424,"tags":10835,"teaser":741,"__hash__":10837,"score":1060},"posts/blog/software-development/connecting-your-max-for-live-device-to-a-cloud-api.md",[8222],{"name":906,"to":907,"avatar":8223},{"src":909},{"label":8225,"color":8226},"MusicTech","#9C27B0",{"type":743,"value":8228,"toc":10779},[8229,8233,8240,8247,8269,8272,8276,8279,8333,8336,8338,8342,8355,8368,8375,8389,8393,8449,8451,8455,8458,8462,8465,8470,8746,8751,8934,8938,8949,8951,8955,8958,8962,8983,8987,8990,9479,9489,9491,9495,9502,9508,9511,9517,9522,9524,9528,9533,9903,9912,9914,9918,9921,10251,10256,10278,10280,10284,10291,10297,10301,10365,10369,10372,10382,10385,10387,10391,10395,10398,10414,10418,10421,10437,10441,10444,10460,10462,10514,10516,10520,10530,10567,10570,10572,10576,10580,10603,10607,10628,10630,10634,10637,10700,10702,10706,10709,10740,10742,10746,10760,10762,10766,10769,10776],[753,8230,8232],{"id":8231},"introduction","Introduction",[746,8234,8235,8236,8239],{},"In our ",[922,8237,8238],{"href":475},"previous article",", we built a Max for Live device that exports arrangement locators to JSON. The data stayed local - displayed in the device UI or saved to a file.",[746,8241,8242,8243,8246],{},"Now let's take it further: ",[970,8244,8245],{},"sending that data to a cloud API"," where it can be stored, visualized, and integrated with other tools.",[8248,8249,8253],"div",{"className":8250},[8251,8252],"flex","justify-center",[8248,8254,8260],{"className":8255},[8256,8257,8258,8259],"bg-neutral-800","p-4","rounded-xl","max-w-md",[746,8261,8262],{},[8263,8264],"img",{"alt":8265,"src":8266,"className":8267},"Ableton Live with Max for Live device sending data to Cloud API","/images/blog/musictechlab_blog_ableton-api-full-integration.webp",[8268],"rounded-lg",[8270,8271],"hr",{},[753,8273,8275],{"id":8274},"why-connect-to-an-api","Why Connect to an API?",[746,8277,8278],{},"Local JSON files are useful, but they have limitations:",[946,8280,8281,8291],{},[949,8282,8283],{},[952,8284,8285,8288],{},[955,8286,8287],{},"Local Files",[955,8289,8290],{},"Cloud API",[962,8292,8293,8301,8309,8317,8325],{},[952,8294,8295,8298],{},[967,8296,8297],{},"Manual file management",[967,8299,8300],{},"Automatic storage",[952,8302,8303,8306],{},[967,8304,8305],{},"Single machine access",[967,8307,8308],{},"Access from anywhere",[952,8310,8311,8314],{},[967,8312,8313],{},"No version history",[967,8315,8316],{},"Full export history",[952,8318,8319,8322],{},[967,8320,8321],{},"No visualization",[967,8323,8324],{},"Browser-based timeline view",[952,8326,8327,8330],{},[967,8328,8329],{},"Manual sharing",[967,8331,8332],{},"Team collaboration",[746,8334,8335],{},"By connecting your Max for Live device to an API, every export becomes part of a searchable, visualized database.",[8270,8337],{},[753,8339,8341],{"id":8340},"architecture-overview-c4-model","Architecture Overview (C4 Model)",[746,8343,8344,8345,8350,8351,8354],{},"We use the ",[922,8346,8349],{"href":8347,"rel":8348},"https://c4model.com/",[926],"C4 model"," to document the system architecture. Here's the ",[970,8352,8353],{},"System Landscape"," showing all actors and systems:",[8248,8356,8358],{"className":8357},[8251,8252],[8248,8359,8361],{"className":8360},[8256,8257,8258,8259],[746,8362,8363],{},[8263,8364],{"alt":8365,"src":8366,"className":8367},"C4 System Landscape - Music Producer, Ableton Live, and Export API","/images/blog/musictechlab_blog_ableton-api-c4-landscape.webp",[8268],[746,8369,8370,8371,8374],{},"And the ",[970,8372,8373],{},"Container View"," showing internal components:",[8248,8376,8378],{"className":8377},[8251,8252],[8248,8379,8382],{"className":8380},[8256,8257,8258,8381],"max-w-xs",[746,8383,8384],{},[8263,8385],{"alt":8386,"src":8387,"className":8388},"C4 Container View - Max for Live Device, Export API, Database","/images/blog/musictechlab_blog_ableton-api-c4-containers.webp",[8268],[1482,8390,8392],{"id":8391},"key-components","Key Components",[946,8394,8395,8408],{},[949,8396,8397],{},[952,8398,8399,8402,8405],{},[955,8400,8401],{},"Container",[955,8403,8404],{},"Technology",[955,8406,8407],{},"Responsibility",[962,8409,8410,8423,8436],{},[952,8411,8412,8417,8420],{},[967,8413,8414],{},[970,8415,8416],{},"Max for Live Device",[967,8418,8419],{},"JavaScript, Node for Max",[967,8421,8422],{},"Extracts metadata from Ableton, sends to API",[952,8424,8425,8430,8433],{},[967,8426,8427],{},[970,8428,8429],{},"Export API",[967,8431,8432],{},"FastAPI, Cloud Run",[967,8434,8435],{},"Receives, validates, stores exports",[952,8437,8438,8443,8446],{},[967,8439,8440],{},[970,8441,8442],{},"Database",[967,8444,8445],{},"PostgreSQL (Cloud SQL)",[967,8447,8448],{},"Persistent storage with JSON sections",[8270,8450],{},[753,8452,8454],{"id":8453},"the-api-receiving-ableton-exports","The API: Receiving Ableton Exports",[746,8456,8457],{},"Our API is built with FastAPI and deployed on Google Cloud Run. Here's the core endpoint:",[1482,8459,8461],{"id":8460},"post-apiabletonexports","POST /api/ableton/exports",[746,8463,8464],{},"Receives and stores project metadata from Ableton Live.",[746,8466,8467],{},[970,8468,8469],{},"Request Schema:",[1037,8471,8474],{"className":8472,"code":8473,"language":2359,"meta":865,"style":865},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{\n  \"project\": \"my_track\",\n  \"bpm\": 103.34,\n  \"time_signature\": {\n    \"numerator\": 4,\n    \"denominator\": 4\n  },\n  \"downbeat_seconds\": 0,\n  \"sections\": [\n    { \"label\": \"INTRO\", \"time_seconds\": 0 },\n    { \"label\": \"VERSE\", \"time_seconds\": 16 },\n    { \"label\": \"CHORUS\", \"time_seconds\": 48 }\n  ],\n  \"exported_at\": \"2026-01-27T14:30:00Z\",\n  \"file_path\": \"/Users/producer/Music/my_track.als\"\n}\n",[1043,8475,8476,8481,8502,8518,8531,8547,8561,8566,8581,8594,8630,8664,8699,8704,8724,8742],{"__ignoreMap":865},[826,8477,8478],{"class":1047,"line":1048},[826,8479,8480],{"class":1178},"{\n",[826,8482,8483,8486,8489,8491,8493,8495,8498,8500],{"class":1047,"line":866},[826,8484,8485],{"class":1178},"  \"",[826,8487,8488],{"class":1592},"project",[826,8490,1292],{"class":1178},[826,8492,2182],{"class":1178},[826,8494,1557],{"class":1178},[826,8496,8497],{"class":1182},"my_track",[826,8499,1292],{"class":1178},[826,8501,2159],{"class":1178},[826,8503,8504,8506,8509,8511,8513,8516],{"class":1047,"line":1060},[826,8505,8485],{"class":1178},[826,8507,8508],{"class":1592},"bpm",[826,8510,1292],{"class":1178},[826,8512,2182],{"class":1178},[826,8514,8515],{"class":1573}," 103.34",[826,8517,2159],{"class":1178},[826,8519,8520,8522,8525,8527,8529],{"class":1047,"line":1066},[826,8521,8485],{"class":1178},[826,8523,8524],{"class":1592},"time_signature",[826,8526,1292],{"class":1178},[826,8528,2182],{"class":1178},[826,8530,3815],{"class":1178},[826,8532,8533,8535,8538,8540,8542,8545],{"class":1047,"line":1072},[826,8534,4932],{"class":1178},[826,8536,8537],{"class":1596},"numerator",[826,8539,1292],{"class":1178},[826,8541,2182],{"class":1178},[826,8543,8544],{"class":1573}," 4",[826,8546,2159],{"class":1178},[826,8548,8549,8551,8554,8556,8558],{"class":1047,"line":1218},[826,8550,4932],{"class":1178},[826,8552,8553],{"class":1596},"denominator",[826,8555,1292],{"class":1178},[826,8557,2182],{"class":1178},[826,8559,8560],{"class":1573}," 4\n",[826,8562,8563],{"class":1047,"line":1229},[826,8564,8565],{"class":1178},"  },\n",[826,8567,8568,8570,8573,8575,8577,8579],{"class":1047,"line":1584},[826,8569,8485],{"class":1178},[826,8571,8572],{"class":1592},"downbeat_seconds",[826,8574,1292],{"class":1178},[826,8576,2182],{"class":1178},[826,8578,5006],{"class":1573},[826,8580,2159],{"class":1178},[826,8582,8583,8585,8588,8590,8592],{"class":1047,"line":1589},[826,8584,8485],{"class":1178},[826,8586,8587],{"class":1592},"sections",[826,8589,1292],{"class":1178},[826,8591,2182],{"class":1178},[826,8593,6994],{"class":1178},[826,8595,8596,8599,8601,8603,8605,8607,8609,8612,8614,8616,8618,8621,8623,8625,8627],{"class":1047,"line":1603},[826,8597,8598],{"class":1178},"    {",[826,8600,1557],{"class":1178},[826,8602,5067],{"class":1596},[826,8604,1292],{"class":1178},[826,8606,2182],{"class":1178},[826,8608,1557],{"class":1178},[826,8610,8611],{"class":1182},"INTRO",[826,8613,1292],{"class":1178},[826,8615,1299],{"class":1178},[826,8617,1557],{"class":1178},[826,8619,8620],{"class":1596},"time_seconds",[826,8622,1292],{"class":1178},[826,8624,2182],{"class":1178},[826,8626,5006],{"class":1573},[826,8628,8629],{"class":1178}," },\n",[826,8631,8632,8634,8636,8638,8640,8642,8644,8647,8649,8651,8653,8655,8657,8659,8662],{"class":1047,"line":1615},[826,8633,8598],{"class":1178},[826,8635,1557],{"class":1178},[826,8637,5067],{"class":1596},[826,8639,1292],{"class":1178},[826,8641,2182],{"class":1178},[826,8643,1557],{"class":1178},[826,8645,8646],{"class":1182},"VERSE",[826,8648,1292],{"class":1178},[826,8650,1299],{"class":1178},[826,8652,1557],{"class":1178},[826,8654,8620],{"class":1596},[826,8656,1292],{"class":1178},[826,8658,2182],{"class":1178},[826,8660,8661],{"class":1573}," 16",[826,8663,8629],{"class":1178},[826,8665,8666,8668,8670,8672,8674,8676,8678,8681,8683,8685,8687,8689,8691,8693,8696],{"class":1047,"line":1620},[826,8667,8598],{"class":1178},[826,8669,1557],{"class":1178},[826,8671,5067],{"class":1596},[826,8673,1292],{"class":1178},[826,8675,2182],{"class":1178},[826,8677,1557],{"class":1178},[826,8679,8680],{"class":1182},"CHORUS",[826,8682,1292],{"class":1178},[826,8684,1299],{"class":1178},[826,8686,1557],{"class":1178},[826,8688,8620],{"class":1596},[826,8690,1292],{"class":1178},[826,8692,2182],{"class":1178},[826,8694,8695],{"class":1573}," 48",[826,8697,8698],{"class":1178}," }\n",[826,8700,8701],{"class":1047,"line":1631},[826,8702,8703],{"class":1178},"  ],\n",[826,8705,8706,8708,8711,8713,8715,8717,8720,8722],{"class":1047,"line":1642},[826,8707,8485],{"class":1178},[826,8709,8710],{"class":1592},"exported_at",[826,8712,1292],{"class":1178},[826,8714,2182],{"class":1178},[826,8716,1557],{"class":1178},[826,8718,8719],{"class":1182},"2026-01-27T14:30:00Z",[826,8721,1292],{"class":1178},[826,8723,2159],{"class":1178},[826,8725,8726,8728,8731,8733,8735,8737,8740],{"class":1047,"line":1652},[826,8727,8485],{"class":1178},[826,8729,8730],{"class":1592},"file_path",[826,8732,1292],{"class":1178},[826,8734,2182],{"class":1178},[826,8736,1557],{"class":1178},[826,8738,8739],{"class":1182},"/Users/producer/Music/my_track.als",[826,8741,1563],{"class":1178},[826,8743,8744],{"class":1047,"line":1662},[826,8745,3495],{"class":1178},[746,8747,8748],{},[970,8749,8750],{},"Response:",[1037,8752,8754],{"className":8472,"code":8753,"language":2359,"meta":865,"style":865},"{\n  \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n  \"project_name\": \"my_track\",\n  \"bpm\": 103.34,\n  \"time_signature\": { \"numerator\": 4, \"denominator\": 4 },\n  \"downbeat_seconds\": 0,\n  \"sections\": [...],\n  \"exported_at\": \"2026-01-27T14:30:00Z\",\n  \"created_at\": \"2026-01-27T14:30:05Z\",\n  \"is_duplicate\": false\n}\n",[1043,8755,8756,8760,8779,8798,8812,8848,8862,8879,8897,8916,8930],{"__ignoreMap":865},[826,8757,8758],{"class":1047,"line":1048},[826,8759,8480],{"class":1178},[826,8761,8762,8764,8766,8768,8770,8772,8775,8777],{"class":1047,"line":866},[826,8763,8485],{"class":1178},[826,8765,2779],{"class":1592},[826,8767,1292],{"class":1178},[826,8769,2182],{"class":1178},[826,8771,1557],{"class":1178},[826,8773,8774],{"class":1182},"550e8400-e29b-41d4-a716-446655440000",[826,8776,1292],{"class":1178},[826,8778,2159],{"class":1178},[826,8780,8781,8783,8786,8788,8790,8792,8794,8796],{"class":1047,"line":1060},[826,8782,8485],{"class":1178},[826,8784,8785],{"class":1592},"project_name",[826,8787,1292],{"class":1178},[826,8789,2182],{"class":1178},[826,8791,1557],{"class":1178},[826,8793,8497],{"class":1182},[826,8795,1292],{"class":1178},[826,8797,2159],{"class":1178},[826,8799,8800,8802,8804,8806,8808,8810],{"class":1047,"line":1066},[826,8801,8485],{"class":1178},[826,8803,8508],{"class":1592},[826,8805,1292],{"class":1178},[826,8807,2182],{"class":1178},[826,8809,8515],{"class":1573},[826,8811,2159],{"class":1178},[826,8813,8814,8816,8818,8820,8822,8824,8826,8828,8830,8832,8834,8836,8838,8840,8842,8844,8846],{"class":1047,"line":1072},[826,8815,8485],{"class":1178},[826,8817,8524],{"class":1592},[826,8819,1292],{"class":1178},[826,8821,2182],{"class":1178},[826,8823,2774],{"class":1178},[826,8825,1557],{"class":1178},[826,8827,8537],{"class":1596},[826,8829,1292],{"class":1178},[826,8831,2182],{"class":1178},[826,8833,8544],{"class":1573},[826,8835,1299],{"class":1178},[826,8837,1557],{"class":1178},[826,8839,8553],{"class":1596},[826,8841,1292],{"class":1178},[826,8843,2182],{"class":1178},[826,8845,8544],{"class":1573},[826,8847,8629],{"class":1178},[826,8849,8850,8852,8854,8856,8858,8860],{"class":1047,"line":1218},[826,8851,8485],{"class":1178},[826,8853,8572],{"class":1592},[826,8855,1292],{"class":1178},[826,8857,2182],{"class":1178},[826,8859,5006],{"class":1573},[826,8861,2159],{"class":1178},[826,8863,8864,8866,8868,8870,8872,8874,8877],{"class":1047,"line":1229},[826,8865,8485],{"class":1178},[826,8867,8587],{"class":1592},[826,8869,1292],{"class":1178},[826,8871,2182],{"class":1178},[826,8873,6350],{"class":1178},[826,8875,8876],{"class":1051},"...",[826,8878,3352],{"class":1178},[826,8880,8881,8883,8885,8887,8889,8891,8893,8895],{"class":1047,"line":1584},[826,8882,8485],{"class":1178},[826,8884,8710],{"class":1592},[826,8886,1292],{"class":1178},[826,8888,2182],{"class":1178},[826,8890,1557],{"class":1178},[826,8892,8719],{"class":1182},[826,8894,1292],{"class":1178},[826,8896,2159],{"class":1178},[826,8898,8899,8901,8903,8905,8907,8909,8912,8914],{"class":1047,"line":1589},[826,8900,8485],{"class":1178},[826,8902,7620],{"class":1592},[826,8904,1292],{"class":1178},[826,8906,2182],{"class":1178},[826,8908,1557],{"class":1178},[826,8910,8911],{"class":1182},"2026-01-27T14:30:05Z",[826,8913,1292],{"class":1178},[826,8915,2159],{"class":1178},[826,8917,8918,8920,8923,8925,8927],{"class":1047,"line":1603},[826,8919,8485],{"class":1178},[826,8921,8922],{"class":1592},"is_duplicate",[826,8924,1292],{"class":1178},[826,8926,2182],{"class":1178},[826,8928,8929],{"class":1178}," false\n",[826,8931,8932],{"class":1047,"line":1615},[826,8933,3495],{"class":1178},[1482,8935,8937],{"id":8936},"idempotency","Idempotency",[746,8939,8940,8941,8944,8945,8948],{},"The API is ",[970,8942,8943],{},"idempotent"," - if you accidentally export the same project twice with the same timestamp, it returns the existing record with ",[1043,8946,8947],{},"is_duplicate: true"," instead of creating a duplicate.",[8270,8950],{},[753,8952,8954],{"id":8953},"extending-the-max-for-live-device","Extending the Max for Live Device",[746,8956,8957],{},"Now let's modify our JavaScript to send data to the API.",[1482,8959,8961],{"id":8960},"the-challenge-max-for-live-http-requests","The Challenge: Max for Live HTTP Requests",[746,8963,8964,8965,8968,8969,8972,8973,8968,8976,8979,8980,1282],{},"Max for Live's JavaScript environment doesn't have native ",[1043,8966,8967],{},"fetch()"," or ",[1043,8970,8971],{},"XMLHttpRequest",". Instead, we use the ",[1043,8974,8975],{},"jweb",[1043,8977,8978],{},"maxurl"," objects, or - the simplest approach - shell out to ",[1043,8981,8982],{},"curl",[1482,8984,8986],{"id":8985},"core-logic-marker_exportjs","Core Logic (marker_export.js)",[746,8988,8989],{},"The JavaScript extends our previous export script with API communication:",[1037,8991,8995],{"className":8992,"code":8993,"language":8994,"meta":865,"style":865},"language-javascript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","// Pseudo-code: marker_export.js\n\nfunction bang() {\n  // 1. Get project data from Live via LiveAPI\n  song = new LiveAPI(\"live_set\")\n\n  name = song.get(\"name\")           // Project name\n  bpm = song.get(\"tempo\")           // BPM\n  sigNum = song.get(\"signature_numerator\")\n  sigDen = song.get(\"signature_denominator\")\n\n  // 2. Collect all locators (cue points)\n  locatorIds = song.get(\"cue_points\")\n  sections = []\n\n  for each locatorId:\n    locator = new LiveAPI(\"id \" + locatorId)\n    sections.push({\n      label: locator.get(\"name\"),      // \"INTRO\", \"VERSE\", etc.\n      time_seconds: locator.get(\"time\") // Position in seconds\n    })\n\n  // 3. Build payload matching API schema\n  payload = {\n    project: name,\n    bpm: bpm,\n    time_signature: { numerator: sigNum, denominator: sigDen },\n    sections: sections,\n    exported_at: now()\n  }\n\n  // 4. Display in UI + send to API\n  outlet(0, JSON.stringify(payload))  // → textedit (preview)\n  outlet(1, payload)                   // → node.script (API sender)\n}\n","javascript",[1043,8996,8997,9002,9006,9019,9024,9048,9052,9080,9107,9131,9155,9159,9164,9188,9197,9201,9214,9241,9255,9284,9312,9319,9323,9328,9337,9348,9360,9389,9400,9412,9417,9421,9426,9456,9475],{"__ignoreMap":865},[826,8998,8999],{"class":1047,"line":1048},[826,9000,9001],{"class":1169},"// Pseudo-code: marker_export.js\n",[826,9003,9004],{"class":1047,"line":866},[826,9005,1547],{"emptyLinePlaceholder":877},[826,9007,9008,9011,9014,9017],{"class":1047,"line":1060},[826,9009,9010],{"class":1592},"function",[826,9012,9013],{"class":1285}," bang",[826,9015,9016],{"class":1178},"()",[826,9018,3815],{"class":1178},[826,9020,9021],{"class":1047,"line":1066},[826,9022,9023],{"class":1169},"  // 1. Get project data from Live via LiveAPI\n",[826,9025,9026,9029,9031,9034,9037,9039,9041,9044,9046],{"class":1047,"line":1072},[826,9027,9028],{"class":1051},"  song",[826,9030,1763],{"class":1178},[826,9032,9033],{"class":1178}," new",[826,9035,9036],{"class":1285}," LiveAPI",[826,9038,1289],{"class":1732},[826,9040,1292],{"class":1178},[826,9042,9043],{"class":1182},"live_set",[826,9045,1292],{"class":1178},[826,9047,1314],{"class":1732},[826,9049,9050],{"class":1047,"line":1218},[826,9051,1547],{"emptyLinePlaceholder":877},[826,9053,9054,9057,9059,9062,9064,9066,9068,9070,9072,9074,9077],{"class":1047,"line":1229},[826,9055,9056],{"class":1051},"  name",[826,9058,1763],{"class":1178},[826,9060,9061],{"class":1051}," song",[826,9063,1282],{"class":1178},[826,9065,2051],{"class":1285},[826,9067,1289],{"class":1732},[826,9069,1292],{"class":1178},[826,9071,5322],{"class":1182},[826,9073,1292],{"class":1178},[826,9075,9076],{"class":1732},")           ",[826,9078,9079],{"class":1169},"// Project name\n",[826,9081,9082,9085,9087,9089,9091,9093,9095,9097,9100,9102,9104],{"class":1047,"line":1584},[826,9083,9084],{"class":1051},"  bpm",[826,9086,1763],{"class":1178},[826,9088,9061],{"class":1051},[826,9090,1282],{"class":1178},[826,9092,2051],{"class":1285},[826,9094,1289],{"class":1732},[826,9096,1292],{"class":1178},[826,9098,9099],{"class":1182},"tempo",[826,9101,1292],{"class":1178},[826,9103,9076],{"class":1732},[826,9105,9106],{"class":1169},"// BPM\n",[826,9108,9109,9112,9114,9116,9118,9120,9122,9124,9127,9129],{"class":1047,"line":1589},[826,9110,9111],{"class":1051},"  sigNum",[826,9113,1763],{"class":1178},[826,9115,9061],{"class":1051},[826,9117,1282],{"class":1178},[826,9119,2051],{"class":1285},[826,9121,1289],{"class":1732},[826,9123,1292],{"class":1178},[826,9125,9126],{"class":1182},"signature_numerator",[826,9128,1292],{"class":1178},[826,9130,1314],{"class":1732},[826,9132,9133,9136,9138,9140,9142,9144,9146,9148,9151,9153],{"class":1047,"line":1603},[826,9134,9135],{"class":1051},"  sigDen",[826,9137,1763],{"class":1178},[826,9139,9061],{"class":1051},[826,9141,1282],{"class":1178},[826,9143,2051],{"class":1285},[826,9145,1289],{"class":1732},[826,9147,1292],{"class":1178},[826,9149,9150],{"class":1182},"signature_denominator",[826,9152,1292],{"class":1178},[826,9154,1314],{"class":1732},[826,9156,9157],{"class":1047,"line":1615},[826,9158,1547],{"emptyLinePlaceholder":877},[826,9160,9161],{"class":1047,"line":1620},[826,9162,9163],{"class":1169},"  // 2. Collect all locators (cue points)\n",[826,9165,9166,9169,9171,9173,9175,9177,9179,9181,9184,9186],{"class":1047,"line":1631},[826,9167,9168],{"class":1051},"  locatorIds",[826,9170,1763],{"class":1178},[826,9172,9061],{"class":1051},[826,9174,1282],{"class":1178},[826,9176,2051],{"class":1285},[826,9178,1289],{"class":1732},[826,9180,1292],{"class":1178},[826,9182,9183],{"class":1182},"cue_points",[826,9185,1292],{"class":1178},[826,9187,1314],{"class":1732},[826,9189,9190,9193,9195],{"class":1047,"line":1642},[826,9191,9192],{"class":1051},"  sections",[826,9194,1763],{"class":1178},[826,9196,6365],{"class":1732},[826,9198,9199],{"class":1047,"line":1652},[826,9200,1547],{"emptyLinePlaceholder":877},[826,9202,9203,9206,9209,9212],{"class":1047,"line":1662},[826,9204,9205],{"class":1051},"  for",[826,9207,9208],{"class":1051}," each",[826,9210,9211],{"class":1596}," locatorId",[826,9213,1600],{"class":1178},[826,9215,9216,9219,9221,9223,9225,9227,9229,9232,9234,9237,9239],{"class":1047,"line":1672},[826,9217,9218],{"class":1051},"    locator",[826,9220,1763],{"class":1178},[826,9222,9033],{"class":1178},[826,9224,9036],{"class":1285},[826,9226,1289],{"class":1732},[826,9228,1292],{"class":1178},[826,9230,9231],{"class":1182},"id ",[826,9233,1292],{"class":1178},[826,9235,9236],{"class":1178}," +",[826,9238,9211],{"class":1051},[826,9240,1314],{"class":1732},[826,9242,9243,9246,9248,9251,9253],{"class":1047,"line":1677},[826,9244,9245],{"class":1051},"    sections",[826,9247,1282],{"class":1178},[826,9249,9250],{"class":1285},"push",[826,9252,1289],{"class":1732},[826,9254,8480],{"class":1178},[826,9256,9257,9260,9262,9265,9267,9269,9271,9273,9275,9277,9279,9281],{"class":1047,"line":1686},[826,9258,9259],{"class":1732},"      label",[826,9261,2182],{"class":1178},[826,9263,9264],{"class":1051}," locator",[826,9266,1282],{"class":1178},[826,9268,2051],{"class":1285},[826,9270,1289],{"class":1732},[826,9272,1292],{"class":1178},[826,9274,5322],{"class":1182},[826,9276,1292],{"class":1178},[826,9278,1150],{"class":1732},[826,9280,1299],{"class":1178},[826,9282,9283],{"class":1169},"      // \"INTRO\", \"VERSE\", etc.\n",[826,9285,9286,9289,9291,9293,9295,9297,9299,9301,9304,9306,9309],{"class":1047,"line":1710},[826,9287,9288],{"class":1732},"      time_seconds",[826,9290,2182],{"class":1178},[826,9292,9264],{"class":1051},[826,9294,1282],{"class":1178},[826,9296,2051],{"class":1285},[826,9298,1289],{"class":1732},[826,9300,1292],{"class":1178},[826,9302,9303],{"class":1182},"time",[826,9305,1292],{"class":1178},[826,9307,9308],{"class":1732},") ",[826,9310,9311],{"class":1169},"// Position in seconds\n",[826,9313,9314,9317],{"class":1047,"line":1721},[826,9315,9316],{"class":1178},"    }",[826,9318,1314],{"class":1732},[826,9320,9321],{"class":1047,"line":1738},[826,9322,1547],{"emptyLinePlaceholder":877},[826,9324,9325],{"class":1047,"line":1747},[826,9326,9327],{"class":1169},"  // 3. Build payload matching API schema\n",[826,9329,9330,9333,9335],{"class":1047,"line":1752},[826,9331,9332],{"class":1051},"  payload",[826,9334,1763],{"class":1178},[826,9336,3815],{"class":1178},[826,9338,9339,9342,9344,9346],{"class":1047,"line":1789},[826,9340,9341],{"class":1732},"    project",[826,9343,2182],{"class":1178},[826,9345,7023],{"class":1051},[826,9347,2159],{"class":1178},[826,9349,9350,9353,9355,9358],{"class":1047,"line":1821},[826,9351,9352],{"class":1732},"    bpm",[826,9354,2182],{"class":1178},[826,9356,9357],{"class":1051}," bpm",[826,9359,2159],{"class":1178},[826,9361,9362,9365,9367,9369,9372,9374,9377,9379,9382,9384,9387],{"class":1047,"line":1853},[826,9363,9364],{"class":1732},"    time_signature",[826,9366,2182],{"class":1178},[826,9368,2774],{"class":1178},[826,9370,9371],{"class":1732}," numerator",[826,9373,2182],{"class":1178},[826,9375,9376],{"class":1051}," sigNum",[826,9378,1299],{"class":1178},[826,9380,9381],{"class":1732}," denominator",[826,9383,2182],{"class":1178},[826,9385,9386],{"class":1051}," sigDen",[826,9388,8629],{"class":1178},[826,9390,9391,9393,9395,9398],{"class":1047,"line":1885},[826,9392,9245],{"class":1732},[826,9394,2182],{"class":1178},[826,9396,9397],{"class":1051}," sections",[826,9399,2159],{"class":1178},[826,9401,9402,9405,9407,9410],{"class":1047,"line":1890},[826,9403,9404],{"class":1732},"    exported_at",[826,9406,2182],{"class":1178},[826,9408,9409],{"class":1285}," now",[826,9411,2343],{"class":1732},[826,9413,9414],{"class":1047,"line":1937},[826,9415,9416],{"class":1178},"  }\n",[826,9418,9419],{"class":1047,"line":1958},[826,9420,1547],{"emptyLinePlaceholder":877},[826,9422,9423],{"class":1047,"line":1965},[826,9424,9425],{"class":1169},"  // 4. Display in UI + send to API\n",[826,9427,9428,9431,9433,9436,9438,9441,9443,9446,9448,9450,9453],{"class":1047,"line":1970},[826,9429,9430],{"class":1285},"  outlet",[826,9432,1289],{"class":1732},[826,9434,9435],{"class":1573},"0",[826,9437,1299],{"class":1178},[826,9439,9440],{"class":1051}," JSON",[826,9442,1282],{"class":1178},[826,9444,9445],{"class":1285},"stringify",[826,9447,1289],{"class":1732},[826,9449,3671],{"class":1051},[826,9451,9452],{"class":1732},"))  ",[826,9454,9455],{"class":1169},"// → textedit (preview)\n",[826,9457,9458,9460,9462,9464,9466,9469,9472],{"class":1047,"line":1983},[826,9459,9430],{"class":1285},[826,9461,1289],{"class":1732},[826,9463,5342],{"class":1573},[826,9465,1299],{"class":1178},[826,9467,9468],{"class":1051}," payload",[826,9470,9471],{"class":1732},")                   ",[826,9473,9474],{"class":1169},"// → node.script (API sender)\n",[826,9476,9477],{"class":1047,"line":1991},[826,9478,3495],{"class":1178},[746,9480,9481,9484,9485,9488],{},[970,9482,9483],{},"Key insight:"," LiveAPI returns arrays for most properties, so always handle ",[1043,9486,9487],{},"Array.isArray()"," checks.",[8270,9490],{},[753,9492,9494],{"id":9493},"alternative-using-nodescript","Alternative: Using node.script",[746,9496,9497,9498,9501],{},"For a more robust solution, use the ",[1043,9499,9500],{},"node.script"," object which runs a Node.js script directly within Max:",[746,9503,9504],{},[8263,9505],{"alt":9506,"src":9507},"Max for Live patcher with node.script for API communication","/images/blog/musictechlab_blog_ableton-api-max-patcher.webp",[746,9509,9510],{},"The patcher architecture:",[1037,9512,9515],{"className":9513,"code":9514,"language":4882},[4880],"┌─────────────────┐     ┌──────────────────────┐     ┌────────────────┐\n│   textbutton    │────▶│  js marker_export.js │────▶│   textedit     │\n│    \"Export\"     │     │                      │     │  (JSON output) │\n└─────────────────┘     └──────────┬───────────┘     └────────────────┘\n                                   │\n                                   ▼\n                        ┌──────────────────────────────────────┐\n                        │  node.script api_sender.js           │\n                        │  @autostart 1 @outlets 1             │\n                        └──────────────────┬───────────────────┘\n                                           │\n                                           ▼\n                                 ┌────────────────┐\n                                 │   textedit     │\n                                 │ (API status)   │\n                                 └────────────────┘\n",[1043,9516,9514],{"__ignoreMap":865},[746,9518,1243,9519,9521],{},[1043,9520,9500],{}," object runs a Node.js script within Max, providing full access to npm packages and modern JavaScript features.",[8270,9523],{},[753,9525,9527],{"id":9526},"the-api-sender-api_senderjs","The API Sender (api_sender.js)",[746,9529,1243,9530,9532],{},[1043,9531,9500],{}," object runs a Node.js script that handles HTTP communication:",[1037,9534,9536],{"className":8992,"code":9535,"language":8994,"meta":865,"style":865},"// Pseudo-code: api_sender.js\n\nconst Max = require('max-api');\nconst API_URL = \"https://your-api.run.app/api/ableton/exports\";\n\nMax.addHandler(\"export\", async (jsonString) => {\n  // 1. Parse incoming data from marker_export.js\n  const payload = JSON.parse(jsonString);\n\n  // 2. POST to API\n  const response = await fetch(API_URL, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify(payload)\n  });\n\n  // 3. Handle response\n  const result = await response.json();\n\n  if (result.is_duplicate) {\n    Max.outlet(\"status\", \"Already exported\");\n  } else {\n    Max.outlet(\"status\", \"Saved: \" + result.id);\n  }\n});\n",[1043,9537,9538,9543,9547,9574,9592,9596,9632,9637,9661,9665,9670,9694,9710,9735,9754,9763,9767,9772,9792,9796,9814,9845,9854,9891,9895],{"__ignoreMap":865},[826,9539,9540],{"class":1047,"line":1048},[826,9541,9542],{"class":1169},"// Pseudo-code: api_sender.js\n",[826,9544,9545],{"class":1047,"line":866},[826,9546,1547],{"emptyLinePlaceholder":877},[826,9548,9549,9552,9555,9557,9560,9562,9564,9567,9569,9571],{"class":1047,"line":1060},[826,9550,9551],{"class":1592},"const",[826,9553,9554],{"class":1051}," Max ",[826,9556,1179],{"class":1178},[826,9558,9559],{"class":1285}," require",[826,9561,1289],{"class":1051},[826,9563,7358],{"class":1178},[826,9565,9566],{"class":1182},"max-api",[826,9568,7358],{"class":1178},[826,9570,1150],{"class":1051},[826,9572,9573],{"class":1178},";\n",[826,9575,9576,9578,9581,9583,9585,9588,9590],{"class":1047,"line":1066},[826,9577,9551],{"class":1592},[826,9579,9580],{"class":1051}," API_URL ",[826,9582,1179],{"class":1178},[826,9584,1557],{"class":1178},[826,9586,9587],{"class":1182},"https://your-api.run.app/api/ableton/exports",[826,9589,1292],{"class":1178},[826,9591,9573],{"class":1178},[826,9593,9594],{"class":1047,"line":1072},[826,9595,1547],{"emptyLinePlaceholder":877},[826,9597,9598,9601,9603,9606,9608,9610,9613,9615,9617,9620,9622,9625,9627,9630],{"class":1047,"line":1218},[826,9599,9600],{"class":1051},"Max",[826,9602,1282],{"class":1178},[826,9604,9605],{"class":1285},"addHandler",[826,9607,1289],{"class":1051},[826,9609,1292],{"class":1178},[826,9611,9612],{"class":1182},"export",[826,9614,1292],{"class":1178},[826,9616,1299],{"class":1178},[826,9618,9619],{"class":1592}," async",[826,9621,2732],{"class":1178},[826,9623,9624],{"class":1302},"jsonString",[826,9626,1150],{"class":1178},[826,9628,9629],{"class":1592}," =>",[826,9631,3815],{"class":1178},[826,9633,9634],{"class":1047,"line":1229},[826,9635,9636],{"class":1169},"  // 1. Parse incoming data from marker_export.js\n",[826,9638,9639,9642,9644,9646,9648,9650,9653,9655,9657,9659],{"class":1047,"line":1584},[826,9640,9641],{"class":1592},"  const",[826,9643,9468],{"class":1051},[826,9645,1763],{"class":1178},[826,9647,9440],{"class":1051},[826,9649,1282],{"class":1178},[826,9651,9652],{"class":1285},"parse",[826,9654,1289],{"class":1732},[826,9656,9624],{"class":1051},[826,9658,1150],{"class":1732},[826,9660,9573],{"class":1178},[826,9662,9663],{"class":1047,"line":1589},[826,9664,1547],{"emptyLinePlaceholder":877},[826,9666,9667],{"class":1047,"line":1603},[826,9668,9669],{"class":1169},"  // 2. POST to API\n",[826,9671,9672,9674,9677,9679,9682,9685,9687,9690,9692],{"class":1047,"line":1615},[826,9673,9641],{"class":1592},[826,9675,9676],{"class":1051}," response",[826,9678,1763],{"class":1178},[826,9680,9681],{"class":1499}," await",[826,9683,9684],{"class":1285}," fetch",[826,9686,1289],{"class":1732},[826,9688,9689],{"class":1051},"API_URL",[826,9691,1299],{"class":1178},[826,9693,3815],{"class":1178},[826,9695,9696,9699,9701,9704,9706,9708],{"class":1047,"line":1620},[826,9697,9698],{"class":1732},"    method",[826,9700,2182],{"class":1178},[826,9702,9703],{"class":1178}," '",[826,9705,8069],{"class":1182},[826,9707,7358],{"class":1178},[826,9709,2159],{"class":1178},[826,9711,9712,9715,9717,9719,9721,9723,9725,9727,9729,9731,9733],{"class":1047,"line":1631},[826,9713,9714],{"class":1732},"    headers",[826,9716,2182],{"class":1178},[826,9718,2774],{"class":1178},[826,9720,9703],{"class":1178},[826,9722,2305],{"class":1732},[826,9724,7358],{"class":1178},[826,9726,2182],{"class":1178},[826,9728,9703],{"class":1178},[826,9730,3010],{"class":1182},[826,9732,7358],{"class":1178},[826,9734,8629],{"class":1178},[826,9736,9737,9740,9742,9744,9746,9748,9750,9752],{"class":1047,"line":1642},[826,9738,9739],{"class":1732},"    body",[826,9741,2182],{"class":1178},[826,9743,9440],{"class":1051},[826,9745,1282],{"class":1178},[826,9747,9445],{"class":1285},[826,9749,1289],{"class":1732},[826,9751,3671],{"class":1051},[826,9753,1314],{"class":1732},[826,9755,9756,9759,9761],{"class":1047,"line":1652},[826,9757,9758],{"class":1178},"  }",[826,9760,1150],{"class":1732},[826,9762,9573],{"class":1178},[826,9764,9765],{"class":1047,"line":1662},[826,9766,1547],{"emptyLinePlaceholder":877},[826,9768,9769],{"class":1047,"line":1672},[826,9770,9771],{"class":1169},"  // 3. Handle response\n",[826,9773,9774,9776,9778,9780,9782,9784,9786,9788,9790],{"class":1047,"line":1677},[826,9775,9641],{"class":1592},[826,9777,5403],{"class":1051},[826,9779,1763],{"class":1178},[826,9781,9681],{"class":1499},[826,9783,9676],{"class":1051},[826,9785,1282],{"class":1178},[826,9787,2359],{"class":1285},[826,9789,9016],{"class":1732},[826,9791,9573],{"class":1178},[826,9793,9794],{"class":1047,"line":1686},[826,9795,1547],{"emptyLinePlaceholder":877},[826,9797,9798,9801,9803,9806,9808,9810,9812],{"class":1047,"line":1710},[826,9799,9800],{"class":1499},"  if",[826,9802,2732],{"class":1732},[826,9804,9805],{"class":1051},"result",[826,9807,1282],{"class":1178},[826,9809,8922],{"class":1051},[826,9811,9308],{"class":1732},[826,9813,8480],{"class":1178},[826,9815,9816,9819,9821,9824,9826,9828,9830,9832,9834,9836,9839,9841,9843],{"class":1047,"line":1721},[826,9817,9818],{"class":1051},"    Max",[826,9820,1282],{"class":1178},[826,9822,9823],{"class":1285},"outlet",[826,9825,1289],{"class":1732},[826,9827,1292],{"class":1178},[826,9829,5460],{"class":1182},[826,9831,1292],{"class":1178},[826,9833,1299],{"class":1178},[826,9835,1557],{"class":1178},[826,9837,9838],{"class":1182},"Already exported",[826,9840,1292],{"class":1178},[826,9842,1150],{"class":1732},[826,9844,9573],{"class":1178},[826,9846,9847,9849,9852],{"class":1047,"line":1738},[826,9848,9758],{"class":1178},[826,9850,9851],{"class":1499}," else",[826,9853,3815],{"class":1178},[826,9855,9856,9858,9860,9862,9864,9866,9868,9870,9872,9874,9877,9879,9881,9883,9885,9887,9889],{"class":1047,"line":1747},[826,9857,9818],{"class":1051},[826,9859,1282],{"class":1178},[826,9861,9823],{"class":1285},[826,9863,1289],{"class":1732},[826,9865,1292],{"class":1178},[826,9867,5460],{"class":1182},[826,9869,1292],{"class":1178},[826,9871,1299],{"class":1178},[826,9873,1557],{"class":1178},[826,9875,9876],{"class":1182},"Saved: ",[826,9878,1292],{"class":1178},[826,9880,9236],{"class":1178},[826,9882,5403],{"class":1051},[826,9884,1282],{"class":1178},[826,9886,2779],{"class":1051},[826,9888,1150],{"class":1732},[826,9890,9573],{"class":1178},[826,9892,9893],{"class":1047,"line":1752},[826,9894,9416],{"class":1178},[826,9896,9897,9899,9901],{"class":1047,"line":1789},[826,9898,2153],{"class":1178},[826,9900,1150],{"class":1051},[826,9902,9573],{"class":1178},[746,9904,9905,9908,9909,9911],{},[970,9906,9907],{},"Why node.script?"," Unlike Max's built-in JS, ",[1043,9910,9500],{}," provides full Node.js runtime with npm packages, async/await, and modern fetch API.",[8270,9913],{},[753,9915,9917],{"id":9916},"the-api-implementation","The API Implementation",[746,9919,9920],{},"The backend is a FastAPI service with PostgreSQL storage:",[1037,9922,9924],{"className":1259,"code":9923,"language":1262,"meta":865,"style":865},"# Pseudo-code: FastAPI endpoint\n\n@router.post(\"/ableton/exports\")\nasync def create_export(data: AbletonExportSchema, db: Session):\n\n    # 1. Idempotency check - prevent duplicates\n    existing = db.query(Export).filter(\n        project == data.project,\n        exported_at == data.exported_at\n    ).first()\n\n    if existing:\n        return { ...existing, is_duplicate: True }\n\n    # 2. Store new export\n    export = Export(\n        id = uuid4(),\n        project_name = data.project,\n        bpm = data.bpm,\n        sections = data.sections,    # JSON array\n        exported_at = data.exported_at\n    )\n\n    db.add(export)\n    db.commit()\n\n    return { ...export, is_duplicate: False }\n",[1043,9925,9926,9931,9935,9957,9989,9993,9998,10024,10040,10054,10064,10068,10077,10098,10102,10107,10119,10130,10145,10160,10178,10191,10195,10199,10215,10226,10230],{"__ignoreMap":865},[826,9927,9928],{"class":1047,"line":1048},[826,9929,9930],{"class":1169},"# Pseudo-code: FastAPI endpoint\n",[826,9932,9933],{"class":1047,"line":866},[826,9934,1547],{"emptyLinePlaceholder":877},[826,9936,9937,9939,9942,9944,9946,9948,9950,9953,9955],{"class":1047,"line":1060},[826,9938,2534],{"class":1178},[826,9940,9941],{"class":1285},"router",[826,9943,1282],{"class":1178},[826,9945,2130],{"class":1285},[826,9947,1289],{"class":1178},[826,9949,1292],{"class":1178},[826,9951,9952],{"class":1182},"/ableton/exports",[826,9954,1292],{"class":1178},[826,9956,1314],{"class":1178},[826,9958,9959,9962,9965,9968,9970,9972,9974,9977,9979,9982,9984,9987],{"class":1047,"line":1066},[826,9960,9961],{"class":1592},"async",[826,9963,9964],{"class":1592}," def",[826,9966,9967],{"class":1285}," create_export",[826,9969,1289],{"class":1178},[826,9971,6623],{"class":1302},[826,9973,2182],{"class":1178},[826,9975,9976],{"class":1051}," AbletonExportSchema",[826,9978,1299],{"class":1178},[826,9980,9981],{"class":1302}," db",[826,9983,2182],{"class":1178},[826,9985,9986],{"class":1051}," Session",[826,9988,4230],{"class":1178},[826,9990,9991],{"class":1047,"line":1072},[826,9992,1547],{"emptyLinePlaceholder":877},[826,9994,9995],{"class":1047,"line":1218},[826,9996,9997],{"class":1169},"    # 1. Idempotency check - prevent duplicates\n",[826,9999,10000,10003,10005,10007,10009,10012,10014,10017,10019,10022],{"class":1047,"line":1229},[826,10001,10002],{"class":1051},"    existing ",[826,10004,1179],{"class":1178},[826,10006,9981],{"class":1051},[826,10008,1282],{"class":1178},[826,10010,10011],{"class":1285},"query",[826,10013,1289],{"class":1178},[826,10015,10016],{"class":1285},"Export",[826,10018,5232],{"class":1178},[826,10020,10021],{"class":1285},"filter",[826,10023,2133],{"class":1178},[826,10025,10026,10029,10032,10034,10036,10038],{"class":1047,"line":1584},[826,10027,10028],{"class":1285},"        project ",[826,10030,10031],{"class":1178},"==",[826,10033,2377],{"class":1285},[826,10035,1282],{"class":1178},[826,10037,8488],{"class":1732},[826,10039,2159],{"class":1178},[826,10041,10042,10045,10047,10049,10051],{"class":1047,"line":1589},[826,10043,10044],{"class":1285},"        exported_at ",[826,10046,10031],{"class":1178},[826,10048,2377],{"class":1285},[826,10050,1282],{"class":1178},[826,10052,10053],{"class":1732},"exported_at\n",[826,10055,10056,10059,10062],{"class":1047,"line":1603},[826,10057,10058],{"class":1178},"    ).",[826,10060,10061],{"class":1285},"first",[826,10063,2343],{"class":1178},[826,10065,10066],{"class":1047,"line":1615},[826,10067,1547],{"emptyLinePlaceholder":877},[826,10069,10070,10072,10075],{"class":1047,"line":1620},[826,10071,2608],{"class":1499},[826,10073,10074],{"class":1051}," existing",[826,10076,1600],{"class":1178},[826,10078,10079,10081,10083,10086,10088,10091,10093,10096],{"class":1047,"line":1631},[826,10080,1986],{"class":1499},[826,10082,2774],{"class":1178},[826,10084,10085],{"class":1051}," ...existing",[826,10087,1299],{"class":1178},[826,10089,10090],{"class":1051}," is_duplicate",[826,10092,2182],{"class":1178},[826,10094,10095],{"class":1178}," True",[826,10097,8698],{"class":1178},[826,10099,10100],{"class":1047,"line":1642},[826,10101,1547],{"emptyLinePlaceholder":877},[826,10103,10104],{"class":1047,"line":1652},[826,10105,10106],{"class":1169},"    # 2. Store new export\n",[826,10108,10109,10112,10114,10117],{"class":1047,"line":1662},[826,10110,10111],{"class":1051},"    export ",[826,10113,1179],{"class":1178},[826,10115,10116],{"class":1285}," Export",[826,10118,2133],{"class":1178},[826,10120,10121,10123,10125,10128],{"class":1047,"line":1672},[826,10122,5241],{"class":1302},[826,10124,1763],{"class":1178},[826,10126,10127],{"class":1285}," uuid4",[826,10129,7961],{"class":1178},[826,10131,10132,10135,10137,10139,10141,10143],{"class":1047,"line":1677},[826,10133,10134],{"class":1302},"        project_name",[826,10136,1763],{"class":1178},[826,10138,2377],{"class":1285},[826,10140,1282],{"class":1178},[826,10142,8488],{"class":1732},[826,10144,2159],{"class":1178},[826,10146,10147,10150,10152,10154,10156,10158],{"class":1047,"line":1686},[826,10148,10149],{"class":1302},"        bpm",[826,10151,1763],{"class":1178},[826,10153,2377],{"class":1285},[826,10155,1282],{"class":1178},[826,10157,8508],{"class":1732},[826,10159,2159],{"class":1178},[826,10161,10162,10165,10167,10169,10171,10173,10175],{"class":1047,"line":1710},[826,10163,10164],{"class":1302},"        sections",[826,10166,1763],{"class":1178},[826,10168,2377],{"class":1285},[826,10170,1282],{"class":1178},[826,10172,8587],{"class":1732},[826,10174,1299],{"class":1178},[826,10176,10177],{"class":1169},"    # JSON array\n",[826,10179,10180,10183,10185,10187,10189],{"class":1047,"line":1721},[826,10181,10182],{"class":1302},"        exported_at",[826,10184,1763],{"class":1178},[826,10186,2377],{"class":1285},[826,10188,1282],{"class":1178},[826,10190,10053],{"class":1732},[826,10192,10193],{"class":1047,"line":1738},[826,10194,5251],{"class":1178},[826,10196,10197],{"class":1047,"line":1747},[826,10198,1547],{"emptyLinePlaceholder":877},[826,10200,10201,10204,10206,10209,10211,10213],{"class":1047,"line":1752},[826,10202,10203],{"class":1051},"    db",[826,10205,1282],{"class":1178},[826,10207,10208],{"class":1285},"add",[826,10210,1289],{"class":1178},[826,10212,9612],{"class":1285},[826,10214,1314],{"class":1178},[826,10216,10217,10219,10221,10224],{"class":1047,"line":1789},[826,10218,10203],{"class":1051},[826,10220,1282],{"class":1178},[826,10222,10223],{"class":1285},"commit",[826,10225,2343],{"class":1178},[826,10227,10228],{"class":1047,"line":1821},[826,10229,1547],{"emptyLinePlaceholder":877},[826,10231,10232,10235,10237,10240,10242,10244,10246,10249],{"class":1047,"line":1853},[826,10233,10234],{"class":1499},"    return",[826,10236,2774],{"class":1178},[826,10238,10239],{"class":1051}," ...export",[826,10241,1299],{"class":1178},[826,10243,10090],{"class":1051},[826,10245,2182],{"class":1178},[826,10247,10248],{"class":1178}," False",[826,10250,8698],{"class":1178},[746,10252,10253],{},[970,10254,10255],{},"Key design decisions:",[834,10257,10258,10266,10272],{},[837,10259,10260,10262,10263],{},[970,10261,8937],{}," via unique constraint on ",[1043,10264,10265],{},"(project_name, exported_at)",[837,10267,10268,10271],{},[970,10269,10270],{},"Sections stored as JSON"," for flexibility",[837,10273,10274,10277],{},[970,10275,10276],{},"UUID primary keys"," for distributed systems compatibility",[8270,10279],{},[753,10281,10283],{"id":10282},"browser-ui-visualizing-your-exports","Browser UI: Visualizing Your Exports",[746,10285,10286,10287,10290],{},"The API includes a browser-based UI at ",[1043,10288,10289],{},"/browser"," that shows all your exports:",[746,10292,10293],{},[8263,10294],{"alt":10295,"src":10296},"Browser UI with timeline visualization of Ableton exports","/images/blog/musictechlab_blog_ableton-api-browser-ui.webp",[1482,10298,10300],{"id":10299},"features","Features",[946,10302,10303,10313],{},[949,10304,10305],{},[952,10306,10307,10310],{},[955,10308,10309],{},"Feature",[955,10311,10312],{},"Description",[962,10314,10315,10325,10335,10345,10355],{},[952,10316,10317,10322],{},[967,10318,10319],{},[970,10320,10321],{},"Timeline visualization",[967,10323,10324],{},"Ableton-style arrangement view",[952,10326,10327,10332],{},[967,10328,10329],{},[970,10330,10331],{},"Filters",[967,10333,10334],{},"By project, BPM range, section count",[952,10336,10337,10342],{},[967,10338,10339],{},[970,10340,10341],{},"Search",[967,10343,10344],{},"Find exports by name",[952,10346,10347,10352],{},[967,10348,10349],{},[970,10350,10351],{},"Pagination",[967,10353,10354],{},"Handle large export histories",[952,10356,10357,10362],{},[967,10358,10359],{},[970,10360,10361],{},"JSON inspection",[967,10363,10364],{},"Click to see full export data",[1482,10366,10368],{"id":10367},"timeline-rendering","Timeline Rendering",[746,10370,10371],{},"Each export is visualized as a horizontal timeline with color-coded sections:",[8248,10373,10375,10376],{"className":10374},[8251,8252],"\n  ",[8263,10377],{"src":10378,"alt":10379,"className":10380},"/images/blog/musictechlab_blog_ableton-api-timeline-row.webp","Timeline visualization with color-coded sections",[10381],"max-w-[70%]",[746,10383,10384],{},"The visualization is generated client-side using vanilla JavaScript and CSS Grid, making it lightweight and fast.",[8270,10386],{},[753,10388,10390],{"id":10389},"additional-api-endpoints","Additional API Endpoints",[1482,10392,10394],{"id":10393},"get-apiabletonexports","GET /api/ableton/exports",[746,10396,10397],{},"List all exports with pagination and filtering:",[1037,10399,10401],{"className":1160,"code":10400,"language":1162,"meta":865,"style":865},"curl \"https://api.example.com/api/ableton/exports?project=my_track&page=1&limit=20\"\n",[1043,10402,10403],{"__ignoreMap":865},[826,10404,10405,10407,10409,10412],{"class":1047,"line":1048},[826,10406,8982],{"class":1596},[826,10408,1557],{"class":1178},[826,10410,10411],{"class":1182},"https://api.example.com/api/ableton/exports?project=my_track&page=1&limit=20",[826,10413,1563],{"class":1178},[1482,10415,10417],{"id":10416},"get-apiabletonexportsid","GET /api/ableton/exports/{id}",[746,10419,10420],{},"Get a specific export by ID:",[1037,10422,10424],{"className":1160,"code":10423,"language":1162,"meta":865,"style":865},"curl \"https://api.example.com/api/ableton/exports/550e8400-e29b-41d4-a716-446655440000\"\n",[1043,10425,10426],{"__ignoreMap":865},[826,10427,10428,10430,10432,10435],{"class":1047,"line":1048},[826,10429,8982],{"class":1596},[826,10431,1557],{"class":1178},[826,10433,10434],{"class":1182},"https://api.example.com/api/ableton/exports/550e8400-e29b-41d4-a716-446655440000",[826,10436,1563],{"class":1178},[1482,10438,10440],{"id":10439},"get-apiabletonprojects","GET /api/ableton/projects",[746,10442,10443],{},"Get list of all unique project names:",[1037,10445,10447],{"className":1160,"code":10446,"language":1162,"meta":865,"style":865},"curl \"https://api.example.com/api/ableton/projects\"\n",[1043,10448,10449],{"__ignoreMap":865},[826,10450,10451,10453,10455,10458],{"class":1047,"line":1048},[826,10452,8982],{"class":1596},[826,10454,1557],{"class":1178},[826,10456,10457],{"class":1182},"https://api.example.com/api/ableton/projects",[826,10459,1563],{"class":1178},[746,10461,8750],{},[1037,10463,10465],{"className":8472,"code":10464,"language":2359,"meta":865,"style":865},"{\n  \"projects\": [\"my_track\", \"remix_v2\", \"collab_final\"]\n}\n",[1043,10466,10467,10471,10510],{"__ignoreMap":865},[826,10468,10469],{"class":1047,"line":1048},[826,10470,8480],{"class":1178},[826,10472,10473,10475,10478,10480,10482,10484,10486,10488,10490,10492,10494,10497,10499,10501,10503,10506,10508],{"class":1047,"line":866},[826,10474,8485],{"class":1178},[826,10476,10477],{"class":1592},"projects",[826,10479,1292],{"class":1178},[826,10481,2182],{"class":1178},[826,10483,6350],{"class":1178},[826,10485,1292],{"class":1178},[826,10487,8497],{"class":1182},[826,10489,1292],{"class":1178},[826,10491,1299],{"class":1178},[826,10493,1557],{"class":1178},[826,10495,10496],{"class":1182},"remix_v2",[826,10498,1292],{"class":1178},[826,10500,1299],{"class":1178},[826,10502,1557],{"class":1178},[826,10504,10505],{"class":1182},"collab_final",[826,10507,1292],{"class":1178},[826,10509,2390],{"class":1178},[826,10511,10512],{"class":1047,"line":1060},[826,10513,3495],{"class":1178},[8270,10515],{},[753,10517,10519],{"id":10518},"deployment","Deployment",[746,10521,10522,10523,10526,10527,2182],{},"The API runs on ",[970,10524,10525],{},"Google Cloud Run"," with ",[970,10528,10529],{},"Cloud SQL (PostgreSQL)",[946,10531,10532,10542],{},[949,10533,10534],{},[952,10535,10536,10539],{},[955,10537,10538],{},"Component",[955,10540,10541],{},"Service",[962,10543,10544,10552,10559],{},[952,10545,10546,10549],{},[967,10547,10548],{},"API",[967,10550,10551],{},"Cloud Run (serverless containers)",[952,10553,10554,10556],{},[967,10555,8442],{},[967,10557,10558],{},"Cloud SQL PostgreSQL",[952,10560,10561,10564],{},[967,10562,10563],{},"Region",[967,10565,10566],{},"europe-west1",[746,10568,10569],{},"Local development uses Docker Compose with the same PostgreSQL setup, ensuring dev/prod parity.",[8270,10571],{},[753,10573,10575],{"id":10574},"testing-the-integration","Testing the Integration",[1482,10577,10579],{"id":10578},"from-ableton","From Ableton",[1080,10581,10582,10585,10588,10594,10600],{},[837,10583,10584],{},"Open a project with arrangement locators",[837,10586,10587],{},"Add the Max for Live device to any MIDI track",[837,10589,10590,10591],{},"Click ",[970,10592,10593],{},"\"Export\"",[837,10595,10596,10597,1292],{},"Watch the status panel - should show \"Saved: ",[826,10598,10599],{},"uuid",[837,10601,10602],{},"Open Browser UI - your export appears in the timeline",[1482,10604,10606],{"id":10605},"verify-with-curl","Verify with curl",[1037,10608,10610],{"className":1160,"code":10609,"language":1162,"meta":865,"style":865},"curl \"https://your-api.run.app/api/ableton/projects\"\n# Returns: { \"projects\": [\"your_project_name\", ...] }\n",[1043,10611,10612,10623],{"__ignoreMap":865},[826,10613,10614,10616,10618,10621],{"class":1047,"line":1048},[826,10615,8982],{"class":1596},[826,10617,1557],{"class":1178},[826,10619,10620],{"class":1182},"https://your-api.run.app/api/ableton/projects",[826,10622,1563],{"class":1178},[826,10624,10625],{"class":1047,"line":866},[826,10626,10627],{"class":1169},"# Returns: { \"projects\": [\"your_project_name\", ...] }\n",[8270,10629],{},[753,10631,10633],{"id":10632},"security-considerations","Security Considerations",[746,10635,10636],{},"For a production deployment, consider:",[946,10638,10639,10649],{},[949,10640,10641],{},[952,10642,10643,10646],{},[955,10644,10645],{},"Concern",[955,10647,10648],{},"Solution",[962,10650,10651,10660,10670,10680,10690],{},[952,10652,10653,10657],{},[967,10654,10655],{},[970,10656,992],{},[967,10658,10659],{},"Add API keys or OAuth",[952,10661,10662,10667],{},[967,10663,10664],{},[970,10665,10666],{},"Rate limiting",[967,10668,10669],{},"Prevent abuse",[952,10671,10672,10677],{},[967,10673,10674],{},[970,10675,10676],{},"CORS",[967,10678,10679],{},"Restrict allowed origins",[952,10681,10682,10687],{},[967,10683,10684],{},[970,10685,10686],{},"Input validation",[967,10688,10689],{},"Already handled by Pydantic",[952,10691,10692,10697],{},[967,10693,10694],{},[970,10695,10696],{},"HTTPS",[967,10698,10699],{},"Enforced by Cloud Run",[8270,10701],{},[753,10703,10705],{"id":10704},"whats-next","What's Next?",[746,10707,10708],{},"This pipeline enables several advanced workflows:",[1080,10710,10711,10717,10723,10729,10734],{},[837,10712,10713,10716],{},[970,10714,10715],{},"AI comparison"," - Compare manual annotations with AI-detected structures",[837,10718,10719,10722],{},[970,10720,10721],{},"Cross-DAW sync"," - Export from Ableton, import to REAPER/Logic",[837,10724,10725,10728],{},[970,10726,10727],{},"Version tracking"," - See how your arrangement evolved over time",[837,10730,10731,10733],{},[970,10732,8332],{}," - Share structure data with collaborators",[837,10735,10736,10739],{},[970,10737,10738],{},"Automated processing"," - Trigger downstream tasks on new exports",[8270,10741],{},[753,10743,10745],{"id":10744},"related-articles","Related Articles",[834,10747,10748,10754],{},[837,10749,10750,10753],{},[922,10751,10752],{"href":475},"Exporting Ableton Live Locators to JSON with Max for Live"," - Build the Max for Live device",[837,10755,10756,10759],{},[922,10757,10758],{"href":101},"Automatic Song Structure Analysis – How AI Detects Intro, Verse, and Chorus"," - AI-powered structure detection",[8270,10761],{},[753,10763,10765],{"id":10764},"conclusion","Conclusion",[746,10767,10768],{},"By connecting your Max for Live device to a cloud API, you transform Ableton from an isolated creative tool into a node in a larger data pipeline. Every arrangement decision, every locator you place, becomes structured data that can power visualizations, training datasets, and cross-platform workflows.",[746,10770,10771,10772,10775],{},"The key insight: ",[970,10773,10774],{},"your DAW is a data source",". Treating it that way opens up possibilities that weren't available when everything stayed local.",[8170,10777,10778],{},"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 .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 pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}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 .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}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 .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}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}",{"title":865,"searchDepth":866,"depth":866,"links":10780},[10781,10782,10783,10786,10790,10794,10795,10796,10797,10801,10806,10807,10811,10812,10813,10814],{"id":8231,"depth":866,"text":8232},{"id":8274,"depth":866,"text":8275},{"id":8340,"depth":866,"text":8341,"children":10784},[10785],{"id":8391,"depth":1060,"text":8392},{"id":8453,"depth":866,"text":8454,"children":10787},[10788,10789],{"id":8460,"depth":1060,"text":8461},{"id":8936,"depth":1060,"text":8937},{"id":8953,"depth":866,"text":8954,"children":10791},[10792,10793],{"id":8960,"depth":1060,"text":8961},{"id":8985,"depth":1060,"text":8986},{"id":9493,"depth":866,"text":9494},{"id":9526,"depth":866,"text":9527},{"id":9916,"depth":866,"text":9917},{"id":10282,"depth":866,"text":10283,"children":10798},[10799,10800],{"id":10299,"depth":1060,"text":10300},{"id":10367,"depth":1060,"text":10368},{"id":10389,"depth":866,"text":10390,"children":10802},[10803,10804,10805],{"id":10393,"depth":1060,"text":10394},{"id":10416,"depth":1060,"text":10417},{"id":10439,"depth":1060,"text":10440},{"id":10518,"depth":866,"text":10519},{"id":10574,"depth":866,"text":10575,"children":10808},[10809,10810],{"id":10578,"depth":1060,"text":10579},{"id":10605,"depth":1060,"text":10606},{"id":10632,"depth":866,"text":10633},{"id":10704,"depth":866,"text":10705},{"id":10744,"depth":866,"text":10745},{"id":10764,"depth":866,"text":10765},"2026-01-27T00:00:00.000Z","Send song structure data from Ableton Live to a cloud API. Build a complete DAW-to-database pipeline with real-time visualization.",{"src":10818},"/images/blog/musictechlab_blog_ableton-api-connection.webp",{"enabled":877,"items":10820},[10821,10824,10827,10830],{"text":10822,"icon":10823},"A cloud API turns local Ableton exports into a searchable, versioned database.","i-lucide-database",{"text":10825,"icon":10826},"FastAPI on Cloud Run receives, validates, and stores JSON from Max for Live.","i-lucide-rocket",{"text":10828,"icon":10829},"Every export gets a browser-based timeline visualization accessible from anywhere.","i-lucide-globe",{"text":10831,"icon":10832},"The pipeline: Max for Live extracts metadata, sends to API, stores in PostgreSQL.","i-lucide-layers",{},{"title":422,"description":10816},[10836,894],"music-production","ve9KJaSJswdmeyVXGi-ApoLF6XbRdIXt3m0EvEJIAIE",{"id":10839,"title":474,"authors":10840,"badge":10843,"body":10844,"category":873,"client":741,"date":13994,"description":13995,"extension":876,"faq":741,"featured":69,"featuredOrder":741,"hidden":69,"image":13996,"keyTakeaways":13998,"meta":14011,"navigation":877,"path":475,"seo":14012,"status":741,"stem":476,"tags":14013,"teaser":741,"__hash__":14014,"score":1060},"posts/blog/software-development/exporting-ableton-live-locators-to-json-with-max-for-live.md",[10841],{"name":906,"to":907,"avatar":10842},{"src":909},{"label":8225,"color":8226},{"type":743,"value":10845,"toc":13963},[10846,10850,10857,10860,10874,10877,10881,10884,11111,11114,11128,11132,11135,11179,11182,11185,11191,11211,11215,11233,11237,11240,11257,11261,11270,11310,11325,11329,11332,11360,11364,11367,12281,12285,12291,12297,12314,12322,12331,12335,12338,12350,12354,12358,12361,12366,12391,12395,12398,12403,12446,12450,12453,12461,12493,12497,12509,12512,12523,12528,12558,12562,12565,12765,12775,12786,12790,12794,12797,12803,12806,12820,12824,12836,12846,12850,12861,12865,12877,12908,12912,12919,12944,12946,12949,13012,13016,13019,13945,13947,13950,13957,13960],[10847,10848,10752],"h1",{"id":10849},"exporting-ableton-live-locators-to-json-with-max-for-live",[746,10851,10852,10853,10856],{},"When building music production tools and AI-powered analysis pipelines, one of the first challenges is getting structured data out of your DAW. Today, we'll walk through creating a ",[970,10854,10855],{},"Max for Live device"," that exports arrangement locators from Ableton Live to a clean JSON format.",[746,10858,10859],{},"This article documents a real proof-of-concept we built - a simple \"EXPORT JSON\" button that extracts:",[834,10861,10862,10868],{},[837,10863,10864,10867],{},[970,10865,10866],{},"BPM"," (tempo)",[837,10869,10870,10873],{},[970,10871,10872],{},"Locators/sections"," (INTRO, VERSE, CHORUS, etc.) with timestamps in seconds",[746,10875,10876],{},"The result is a portable JSON file that can feed into visualization tools, AI models, or cross-DAW workflows.",[753,10878,10880],{"id":10879},"the-goal","The Goal",[746,10882,10883],{},"We want a one-click solution inside Ableton Live that produces output like this:",[1037,10885,10887],{"className":8472,"code":10886,"language":2359,"meta":865,"style":865},"{\n  \"project\": \"my_track\",\n  \"bpm\": 103.34,\n  \"sections\": [\n    { \"label\": \"INTRO\", \"time_seconds\": 0 },\n    { \"label\": \"VERSE\", \"time_seconds\": 16 },\n    { \"label\": \"BRIDGE\", \"time_seconds\": 80 },\n    { \"label\": \"CHORUS\", \"time_seconds\": 112 },\n    { \"label\": \"OUTRO\", \"time_seconds\": 224 }\n  ]\n}\n",[1043,10888,10889,10893,10911,10925,10937,10969,11001,11035,11068,11102,11107],{"__ignoreMap":865},[826,10890,10891],{"class":1047,"line":1048},[826,10892,8480],{"class":1178},[826,10894,10895,10897,10899,10901,10903,10905,10907,10909],{"class":1047,"line":866},[826,10896,8485],{"class":1178},[826,10898,8488],{"class":1592},[826,10900,1292],{"class":1178},[826,10902,2182],{"class":1178},[826,10904,1557],{"class":1178},[826,10906,8497],{"class":1182},[826,10908,1292],{"class":1178},[826,10910,2159],{"class":1178},[826,10912,10913,10915,10917,10919,10921,10923],{"class":1047,"line":1060},[826,10914,8485],{"class":1178},[826,10916,8508],{"class":1592},[826,10918,1292],{"class":1178},[826,10920,2182],{"class":1178},[826,10922,8515],{"class":1573},[826,10924,2159],{"class":1178},[826,10926,10927,10929,10931,10933,10935],{"class":1047,"line":1066},[826,10928,8485],{"class":1178},[826,10930,8587],{"class":1592},[826,10932,1292],{"class":1178},[826,10934,2182],{"class":1178},[826,10936,6994],{"class":1178},[826,10938,10939,10941,10943,10945,10947,10949,10951,10953,10955,10957,10959,10961,10963,10965,10967],{"class":1047,"line":1072},[826,10940,8598],{"class":1178},[826,10942,1557],{"class":1178},[826,10944,5067],{"class":1596},[826,10946,1292],{"class":1178},[826,10948,2182],{"class":1178},[826,10950,1557],{"class":1178},[826,10952,8611],{"class":1182},[826,10954,1292],{"class":1178},[826,10956,1299],{"class":1178},[826,10958,1557],{"class":1178},[826,10960,8620],{"class":1596},[826,10962,1292],{"class":1178},[826,10964,2182],{"class":1178},[826,10966,5006],{"class":1573},[826,10968,8629],{"class":1178},[826,10970,10971,10973,10975,10977,10979,10981,10983,10985,10987,10989,10991,10993,10995,10997,10999],{"class":1047,"line":1218},[826,10972,8598],{"class":1178},[826,10974,1557],{"class":1178},[826,10976,5067],{"class":1596},[826,10978,1292],{"class":1178},[826,10980,2182],{"class":1178},[826,10982,1557],{"class":1178},[826,10984,8646],{"class":1182},[826,10986,1292],{"class":1178},[826,10988,1299],{"class":1178},[826,10990,1557],{"class":1178},[826,10992,8620],{"class":1596},[826,10994,1292],{"class":1178},[826,10996,2182],{"class":1178},[826,10998,8661],{"class":1573},[826,11000,8629],{"class":1178},[826,11002,11003,11005,11007,11009,11011,11013,11015,11018,11020,11022,11024,11026,11028,11030,11033],{"class":1047,"line":1229},[826,11004,8598],{"class":1178},[826,11006,1557],{"class":1178},[826,11008,5067],{"class":1596},[826,11010,1292],{"class":1178},[826,11012,2182],{"class":1178},[826,11014,1557],{"class":1178},[826,11016,11017],{"class":1182},"BRIDGE",[826,11019,1292],{"class":1178},[826,11021,1299],{"class":1178},[826,11023,1557],{"class":1178},[826,11025,8620],{"class":1596},[826,11027,1292],{"class":1178},[826,11029,2182],{"class":1178},[826,11031,11032],{"class":1573}," 80",[826,11034,8629],{"class":1178},[826,11036,11037,11039,11041,11043,11045,11047,11049,11051,11053,11055,11057,11059,11061,11063,11066],{"class":1047,"line":1584},[826,11038,8598],{"class":1178},[826,11040,1557],{"class":1178},[826,11042,5067],{"class":1596},[826,11044,1292],{"class":1178},[826,11046,2182],{"class":1178},[826,11048,1557],{"class":1178},[826,11050,8680],{"class":1182},[826,11052,1292],{"class":1178},[826,11054,1299],{"class":1178},[826,11056,1557],{"class":1178},[826,11058,8620],{"class":1596},[826,11060,1292],{"class":1178},[826,11062,2182],{"class":1178},[826,11064,11065],{"class":1573}," 112",[826,11067,8629],{"class":1178},[826,11069,11070,11072,11074,11076,11078,11080,11082,11085,11087,11089,11091,11093,11095,11097,11100],{"class":1047,"line":1589},[826,11071,8598],{"class":1178},[826,11073,1557],{"class":1178},[826,11075,5067],{"class":1596},[826,11077,1292],{"class":1178},[826,11079,2182],{"class":1178},[826,11081,1557],{"class":1178},[826,11083,11084],{"class":1182},"OUTRO",[826,11086,1292],{"class":1178},[826,11088,1299],{"class":1178},[826,11090,1557],{"class":1178},[826,11092,8620],{"class":1596},[826,11094,1292],{"class":1178},[826,11096,2182],{"class":1178},[826,11098,11099],{"class":1573}," 224",[826,11101,8698],{"class":1178},[826,11103,11104],{"class":1047,"line":1603},[826,11105,11106],{"class":1178},"  ]\n",[826,11108,11109],{"class":1047,"line":1615},[826,11110,3495],{"class":1178},[746,11112,11113],{},"This JSON can then be:",[834,11115,11116,11119,11122,11125],{},[837,11117,11118],{},"Imported into other DAWs (REAPER, Logic, etc.)",[837,11120,11121],{},"Used for automated video editing synced to song structure",[837,11123,11124],{},"Fed into machine learning models for training",[837,11126,11127],{},"Displayed in custom visualization tools",[753,11129,11131],{"id":11130},"prerequisites","Prerequisites",[746,11133,11134],{},"To follow along, you'll need:",[946,11136,11137,11147],{},[949,11138,11139],{},[952,11140,11141,11144],{},[955,11142,11143],{},"Requirement",[955,11145,11146],{},"Notes",[962,11148,11149,11159,11169],{},[952,11150,11151,11156],{},[967,11152,11153],{},[970,11154,11155],{},"Ableton Live 12 Suite",[967,11157,11158],{},"Max for Live is included in Suite",[952,11160,11161,11166],{},[967,11162,11163],{},[970,11164,11165],{},"Basic Max/MSP knowledge",[967,11167,11168],{},"We'll keep it simple",[952,11170,11171,11176],{},[967,11172,11173],{},[970,11174,11175],{},"Text editor",[967,11177,11178],{},"For the JavaScript file",[753,11180,11181],{"id":1031},"Architecture Overview",[746,11183,11184],{},"Our device consists of three components:",[1037,11186,11189],{"className":11187,"code":11188,"language":4882},[4880],"┌─────────────────┐     ┌──────────────────────┐     ┌────────────────┐\n│   textbutton    │────▶│  js marker_export.js │────▶│   textedit     │\n│  \"EXPORT JSON\"  │     │   (LiveAPI magic)    │     │  (JSON preview)│\n└─────────────────┘     └──────────────────────┘     └────────────────┘\n",[1043,11190,11188],{"__ignoreMap":865},[1080,11192,11193,11199,11205],{},[837,11194,11195,11198],{},[970,11196,11197],{},"textbutton"," - The user clicks this to trigger export",[837,11200,11201,11204],{},[970,11202,11203],{},"js marker_export.js"," - JavaScript that queries Ableton's Live Object Model",[837,11206,11207,11210],{},[970,11208,11209],{},"textedit"," - Read-only display showing the exported JSON",[753,11212,11214],{"id":11213},"building-the-max-for-live-device","Building the Max for Live Device",[8248,11216,10375,11218],{"style":11217},"display: flex; justify-content: center; margin: 2rem 0;",[11219,11220,11222,11223,11222,11228,10375],"figure",{"style":11221},"margin: 0; text-align: center;","\n    ",[8263,11224],{"src":11225,"alt":11226,"style":11227},"/images/blog/musictechlab_blog_ableton-locators-json-max-patcher.webp","Max for Live patcher with JavaScript code","max-width: 600px; width: 100%; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: block;",[11229,11230,11232],"figcaption",{"style":11231},"margin-top: 0.5rem; color: #666; font-size: 0.875rem;","Max for Live patcher with the JavaScript code editor open",[1482,11234,11236],{"id":11235},"step-1-create-the-patcher","Step 1: Create the Patcher",[746,11238,11239],{},"Open Max for Live in Ableton (create a new Max MIDI Effect), then add these objects:",[1080,11241,11242,11247,11252],{},[837,11243,11244,11246],{},[970,11245,11197],{}," - Set the text to \"EXPORT JSON\"",[837,11248,11249,11251],{},[970,11250,11203],{}," - This will hold our JavaScript logic",[837,11253,11254,11256],{},[970,11255,11209],{}," - Enable \"Read Only\" in the Inspector",[1482,11258,11260],{"id":11259},"step-2-wire-the-connections","Step 2: Wire the Connections",[746,11262,11263,11264,11266,11267,2182],{},"This is where things get tricky. The ",[1043,11265,11197],{}," object has ",[970,11268,11269],{},"three outlets",[946,11271,11272,11282],{},[949,11273,11274],{},[952,11275,11276,11279],{},[955,11277,11278],{},"Outlet",[955,11280,11281],{},"Output",[962,11283,11284,11292,11300],{},[952,11285,11286,11289],{},[967,11287,11288],{},"1st (left)",[967,11290,11291],{},"Button text",[952,11293,11294,11297],{},[967,11295,11296],{},"2nd (middle)",[967,11298,11299],{},"Mouse events",[952,11301,11302,11307],{},[967,11303,11304],{},[970,11305,11306],{},"3rd (right)",[967,11308,11309],{},"Integer (1 on click)",[746,11311,11312,11313,11316,11317,11319,11320,11322,11323,1282],{},"Connect the ",[970,11314,11315],{},"third outlet"," of ",[1043,11318,11197],{}," to the inlet of ",[1043,11321,11203],{},", then connect the outlet of the JS object to ",[1043,11324,11209],{},[1482,11326,11328],{"id":11327},"step-3-set-up-presentation-mode","Step 3: Set Up Presentation Mode",[746,11330,11331],{},"For the device to be usable in Live's UI:",[1080,11333,11334,11341,11344,11350,11357],{},[837,11335,11336,11337,1136,11339],{},"Select both ",[1043,11338,11197],{},[1043,11340,11209],{},[837,11342,11343],{},"Open the Inspector (right sidebar)",[837,11345,11346,11347],{},"Enable ",[970,11348,11349],{},"\"Include in Presentation\"",[837,11351,11352,11353,11356],{},"Switch to ",[970,11354,11355],{},"Presentation Mode"," (View → Presentation Mode)",[837,11358,11359],{},"Arrange the objects nicely",[753,11361,11363],{"id":11362},"the-javascript-marker_exportjs","The JavaScript: marker_export.js",[746,11365,11366],{},"Here's the complete script that powers our device:",[1037,11368,11370],{"className":8992,"code":11369,"language":8994,"meta":865,"style":865},"autowatch = 1;\noutlets = 1;\n\nfunction msg_int(v) {\n  if (v === 1) bang();\n}\n\nfunction bang() {\n  try {\n    var song = new LiveAPI(\"live_set\");\n\n    // --- PROJECT NAME ---\n    var name = song.get(\"name\");\n    name = Array.isArray(name) ? name[0] : name;\n    if (!name || name === \"\") name = \"untitled\";\n    name = name.replace(/[^a-z0-9_\\-]/gi, \"_\");\n\n    // --- BPM ---\n    var bpm = song.get(\"tempo\");\n    bpm = Array.isArray(bpm) ? bpm[0] : bpm;\n\n    // --- LOCATORS ---\n    var locatorIds = song.get(\"cue_points\");\n    var sections = [];\n\n    if (Array.isArray(locatorIds)) {\n      for (var i = 0; i \u003C locatorIds.length; i++) {\n        if (typeof locatorIds[i] !== \"number\") continue;\n\n        var l = new LiveAPI(\"id \" + locatorIds[i]);\n        var label = l.get(\"name\");\n        var time = l.get(\"time\");\n\n        sections.push({\n          label: Array.isArray(label) ? label[0] : label,\n          time_seconds: Array.isArray(time) ? time[0] : time\n        });\n      }\n    }\n\n    // --- BUILD JSON ---\n    var data = {\n      project: name,\n      bpm: bpm,\n      sections: sections\n    };\n\n    var json = JSON.stringify(data, null, 2);\n\n    // Send to UI (textedit)\n    outlet(0, \"set\", json);\n\n    // Log to Max Console\n    post(\"EXPORT SUCCESS\\\\n\");\n\n  } catch (e) {\n    post(\"EXPORT ERROR:\", e.toString(), \"\\\\n\");\n  }\n}\n",[1043,11371,11372,11383,11394,11398,11414,11436,11440,11444,11454,11461,11486,11490,11495,11521,11560,11595,11638,11642,11647,11673,11707,11711,11716,11743,11756,11760,11783,11826,11861,11865,11899,11926,11953,11957,11969,12004,12038,12047,12052,12056,12060,12065,12075,12086,12097,12107,12112,12116,12147,12151,12156,12181,12185,12190,12214,12218,12234,12273,12277],{"__ignoreMap":865},[826,11373,11374,11377,11379,11381],{"class":1047,"line":1048},[826,11375,11376],{"class":1051},"autowatch ",[826,11378,1179],{"class":1178},[826,11380,5797],{"class":1573},[826,11382,9573],{"class":1178},[826,11384,11385,11388,11390,11392],{"class":1047,"line":866},[826,11386,11387],{"class":1051},"outlets ",[826,11389,1179],{"class":1178},[826,11391,5797],{"class":1573},[826,11393,9573],{"class":1178},[826,11395,11396],{"class":1047,"line":1060},[826,11397,1547],{"emptyLinePlaceholder":877},[826,11399,11400,11402,11405,11407,11410,11412],{"class":1047,"line":1066},[826,11401,9010],{"class":1592},[826,11403,11404],{"class":1285}," msg_int",[826,11406,1289],{"class":1178},[826,11408,11409],{"class":1302},"v",[826,11411,1150],{"class":1178},[826,11413,3815],{"class":1178},[826,11415,11416,11418,11420,11422,11425,11427,11429,11432,11434],{"class":1047,"line":1072},[826,11417,9800],{"class":1499},[826,11419,2732],{"class":1732},[826,11421,11409],{"class":1051},[826,11423,11424],{"class":1178}," ===",[826,11426,5797],{"class":1573},[826,11428,9308],{"class":1732},[826,11430,11431],{"class":1285},"bang",[826,11433,9016],{"class":1732},[826,11435,9573],{"class":1178},[826,11437,11438],{"class":1047,"line":1218},[826,11439,3495],{"class":1178},[826,11441,11442],{"class":1047,"line":1229},[826,11443,1547],{"emptyLinePlaceholder":877},[826,11445,11446,11448,11450,11452],{"class":1047,"line":1584},[826,11447,9010],{"class":1592},[826,11449,9013],{"class":1285},[826,11451,9016],{"class":1178},[826,11453,3815],{"class":1178},[826,11455,11456,11459],{"class":1047,"line":1589},[826,11457,11458],{"class":1499},"  try",[826,11460,3815],{"class":1178},[826,11462,11463,11466,11468,11470,11472,11474,11476,11478,11480,11482,11484],{"class":1047,"line":1603},[826,11464,11465],{"class":1592},"    var",[826,11467,9061],{"class":1051},[826,11469,1763],{"class":1178},[826,11471,9033],{"class":1178},[826,11473,9036],{"class":1285},[826,11475,1289],{"class":1732},[826,11477,1292],{"class":1178},[826,11479,9043],{"class":1182},[826,11481,1292],{"class":1178},[826,11483,1150],{"class":1732},[826,11485,9573],{"class":1178},[826,11487,11488],{"class":1047,"line":1615},[826,11489,1547],{"emptyLinePlaceholder":877},[826,11491,11492],{"class":1047,"line":1620},[826,11493,11494],{"class":1169},"    // --- PROJECT NAME ---\n",[826,11496,11497,11499,11501,11503,11505,11507,11509,11511,11513,11515,11517,11519],{"class":1047,"line":1631},[826,11498,11465],{"class":1592},[826,11500,7023],{"class":1051},[826,11502,1763],{"class":1178},[826,11504,9061],{"class":1051},[826,11506,1282],{"class":1178},[826,11508,2051],{"class":1285},[826,11510,1289],{"class":1732},[826,11512,1292],{"class":1178},[826,11514,5322],{"class":1182},[826,11516,1292],{"class":1178},[826,11518,1150],{"class":1732},[826,11520,9573],{"class":1178},[826,11522,11523,11526,11528,11531,11533,11536,11538,11540,11542,11545,11547,11549,11551,11554,11556,11558],{"class":1047,"line":1642},[826,11524,11525],{"class":1051},"    name",[826,11527,1763],{"class":1178},[826,11529,11530],{"class":1051}," Array",[826,11532,1282],{"class":1178},[826,11534,11535],{"class":1285},"isArray",[826,11537,1289],{"class":1732},[826,11539,5322],{"class":1051},[826,11541,9308],{"class":1732},[826,11543,11544],{"class":1178},"?",[826,11546,7023],{"class":1051},[826,11548,2380],{"class":1732},[826,11550,9435],{"class":1573},[826,11552,11553],{"class":1732},"] ",[826,11555,2182],{"class":1178},[826,11557,7023],{"class":1051},[826,11559,9573],{"class":1178},[826,11561,11562,11564,11566,11569,11571,11574,11576,11578,11580,11582,11584,11586,11588,11591,11593],{"class":1047,"line":1652},[826,11563,2608],{"class":1499},[826,11565,2732],{"class":1732},[826,11567,11568],{"class":1178},"!",[826,11570,5322],{"class":1051},[826,11572,11573],{"class":1178}," ||",[826,11575,7023],{"class":1051},[826,11577,11424],{"class":1178},[826,11579,1784],{"class":1178},[826,11581,9308],{"class":1732},[826,11583,5322],{"class":1051},[826,11585,1763],{"class":1178},[826,11587,1557],{"class":1178},[826,11589,11590],{"class":1182},"untitled",[826,11592,1292],{"class":1178},[826,11594,9573],{"class":1178},[826,11596,11597,11599,11601,11603,11605,11608,11610,11613,11616,11619,11622,11625,11627,11629,11632,11634,11636],{"class":1047,"line":1662},[826,11598,11525],{"class":1051},[826,11600,1763],{"class":1178},[826,11602,7023],{"class":1051},[826,11604,1282],{"class":1178},[826,11606,11607],{"class":1285},"replace",[826,11609,1289],{"class":1732},[826,11611,11612],{"class":1178},"/[^",[826,11614,11615],{"class":1182},"a-z0-9_",[826,11617,11618],{"class":1051},"\\-",[826,11620,11621],{"class":1178},"]/",[826,11623,11624],{"class":1573},"gi",[826,11626,1299],{"class":1178},[826,11628,1557],{"class":1178},[826,11630,11631],{"class":1182},"_",[826,11633,1292],{"class":1178},[826,11635,1150],{"class":1732},[826,11637,9573],{"class":1178},[826,11639,11640],{"class":1047,"line":1672},[826,11641,1547],{"emptyLinePlaceholder":877},[826,11643,11644],{"class":1047,"line":1677},[826,11645,11646],{"class":1169},"    // --- BPM ---\n",[826,11648,11649,11651,11653,11655,11657,11659,11661,11663,11665,11667,11669,11671],{"class":1047,"line":1686},[826,11650,11465],{"class":1592},[826,11652,9357],{"class":1051},[826,11654,1763],{"class":1178},[826,11656,9061],{"class":1051},[826,11658,1282],{"class":1178},[826,11660,2051],{"class":1285},[826,11662,1289],{"class":1732},[826,11664,1292],{"class":1178},[826,11666,9099],{"class":1182},[826,11668,1292],{"class":1178},[826,11670,1150],{"class":1732},[826,11672,9573],{"class":1178},[826,11674,11675,11677,11679,11681,11683,11685,11687,11689,11691,11693,11695,11697,11699,11701,11703,11705],{"class":1047,"line":1710},[826,11676,9352],{"class":1051},[826,11678,1763],{"class":1178},[826,11680,11530],{"class":1051},[826,11682,1282],{"class":1178},[826,11684,11535],{"class":1285},[826,11686,1289],{"class":1732},[826,11688,8508],{"class":1051},[826,11690,9308],{"class":1732},[826,11692,11544],{"class":1178},[826,11694,9357],{"class":1051},[826,11696,2380],{"class":1732},[826,11698,9435],{"class":1573},[826,11700,11553],{"class":1732},[826,11702,2182],{"class":1178},[826,11704,9357],{"class":1051},[826,11706,9573],{"class":1178},[826,11708,11709],{"class":1047,"line":1721},[826,11710,1547],{"emptyLinePlaceholder":877},[826,11712,11713],{"class":1047,"line":1738},[826,11714,11715],{"class":1169},"    // --- LOCATORS ---\n",[826,11717,11718,11720,11723,11725,11727,11729,11731,11733,11735,11737,11739,11741],{"class":1047,"line":1747},[826,11719,11465],{"class":1592},[826,11721,11722],{"class":1051}," locatorIds",[826,11724,1763],{"class":1178},[826,11726,9061],{"class":1051},[826,11728,1282],{"class":1178},[826,11730,2051],{"class":1285},[826,11732,1289],{"class":1732},[826,11734,1292],{"class":1178},[826,11736,9183],{"class":1182},[826,11738,1292],{"class":1178},[826,11740,1150],{"class":1732},[826,11742,9573],{"class":1178},[826,11744,11745,11747,11749,11751,11754],{"class":1047,"line":1752},[826,11746,11465],{"class":1592},[826,11748,9397],{"class":1051},[826,11750,1763],{"class":1178},[826,11752,11753],{"class":1732}," []",[826,11755,9573],{"class":1178},[826,11757,11758],{"class":1047,"line":1789},[826,11759,1547],{"emptyLinePlaceholder":877},[826,11761,11762,11764,11766,11769,11771,11773,11775,11778,11781],{"class":1047,"line":1821},[826,11763,2608],{"class":1499},[826,11765,2732],{"class":1732},[826,11767,11768],{"class":1051},"Array",[826,11770,1282],{"class":1178},[826,11772,11535],{"class":1285},[826,11774,1289],{"class":1732},[826,11776,11777],{"class":1051},"locatorIds",[826,11779,11780],{"class":1732},")) ",[826,11782,8480],{"class":1178},[826,11784,11785,11788,11790,11793,11796,11798,11800,11803,11805,11808,11810,11812,11815,11817,11819,11822,11824],{"class":1047,"line":1853},[826,11786,11787],{"class":1499},"      for",[826,11789,2732],{"class":1732},[826,11791,11792],{"class":1592},"var",[826,11794,11795],{"class":1051}," i",[826,11797,1763],{"class":1178},[826,11799,5006],{"class":1573},[826,11801,11802],{"class":1178},";",[826,11804,11795],{"class":1051},[826,11806,11807],{"class":1178}," \u003C",[826,11809,11722],{"class":1051},[826,11811,1282],{"class":1178},[826,11813,11814],{"class":1051},"length",[826,11816,11802],{"class":1178},[826,11818,11795],{"class":1051},[826,11820,11821],{"class":1178},"++",[826,11823,9308],{"class":1732},[826,11825,8480],{"class":1178},[826,11827,11828,11830,11832,11835,11837,11839,11842,11844,11847,11849,11852,11854,11856,11859],{"class":1047,"line":1885},[826,11829,1724],{"class":1499},[826,11831,2732],{"class":1732},[826,11833,11834],{"class":1178},"typeof",[826,11836,11722],{"class":1051},[826,11838,2380],{"class":1732},[826,11840,11841],{"class":1051},"i",[826,11843,11553],{"class":1732},[826,11845,11846],{"class":1178},"!==",[826,11848,1557],{"class":1178},[826,11850,11851],{"class":1182},"number",[826,11853,1292],{"class":1178},[826,11855,9308],{"class":1732},[826,11857,11858],{"class":1499},"continue",[826,11860,9573],{"class":1178},[826,11862,11863],{"class":1047,"line":1890},[826,11864,1547],{"emptyLinePlaceholder":877},[826,11866,11867,11870,11873,11875,11877,11879,11881,11883,11885,11887,11889,11891,11893,11895,11897],{"class":1047,"line":1937},[826,11868,11869],{"class":1592},"        var",[826,11871,11872],{"class":1051}," l",[826,11874,1763],{"class":1178},[826,11876,9033],{"class":1178},[826,11878,9036],{"class":1285},[826,11880,1289],{"class":1732},[826,11882,1292],{"class":1178},[826,11884,9231],{"class":1182},[826,11886,1292],{"class":1178},[826,11888,9236],{"class":1178},[826,11890,11722],{"class":1051},[826,11892,2380],{"class":1732},[826,11894,11841],{"class":1051},[826,11896,2857],{"class":1732},[826,11898,9573],{"class":1178},[826,11900,11901,11903,11906,11908,11910,11912,11914,11916,11918,11920,11922,11924],{"class":1047,"line":1958},[826,11902,11869],{"class":1592},[826,11904,11905],{"class":1051}," label",[826,11907,1763],{"class":1178},[826,11909,11872],{"class":1051},[826,11911,1282],{"class":1178},[826,11913,2051],{"class":1285},[826,11915,1289],{"class":1732},[826,11917,1292],{"class":1178},[826,11919,5322],{"class":1182},[826,11921,1292],{"class":1178},[826,11923,1150],{"class":1732},[826,11925,9573],{"class":1178},[826,11927,11928,11930,11933,11935,11937,11939,11941,11943,11945,11947,11949,11951],{"class":1047,"line":1965},[826,11929,11869],{"class":1592},[826,11931,11932],{"class":1051}," time",[826,11934,1763],{"class":1178},[826,11936,11872],{"class":1051},[826,11938,1282],{"class":1178},[826,11940,2051],{"class":1285},[826,11942,1289],{"class":1732},[826,11944,1292],{"class":1178},[826,11946,9303],{"class":1182},[826,11948,1292],{"class":1178},[826,11950,1150],{"class":1732},[826,11952,9573],{"class":1178},[826,11954,11955],{"class":1047,"line":1970},[826,11956,1547],{"emptyLinePlaceholder":877},[826,11958,11959,11961,11963,11965,11967],{"class":1047,"line":1983},[826,11960,10164],{"class":1051},[826,11962,1282],{"class":1178},[826,11964,9250],{"class":1285},[826,11966,1289],{"class":1732},[826,11968,8480],{"class":1178},[826,11970,11971,11974,11976,11978,11980,11982,11984,11986,11988,11990,11992,11994,11996,11998,12000,12002],{"class":1047,"line":1991},[826,11972,11973],{"class":1732},"          label",[826,11975,2182],{"class":1178},[826,11977,11530],{"class":1051},[826,11979,1282],{"class":1178},[826,11981,11535],{"class":1285},[826,11983,1289],{"class":1732},[826,11985,5067],{"class":1051},[826,11987,9308],{"class":1732},[826,11989,11544],{"class":1178},[826,11991,11905],{"class":1051},[826,11993,2380],{"class":1732},[826,11995,9435],{"class":1573},[826,11997,11553],{"class":1732},[826,11999,2182],{"class":1178},[826,12001,11905],{"class":1051},[826,12003,2159],{"class":1178},[826,12005,12006,12009,12011,12013,12015,12017,12019,12021,12023,12025,12027,12029,12031,12033,12035],{"class":1047,"line":1996},[826,12007,12008],{"class":1732},"          time_seconds",[826,12010,2182],{"class":1178},[826,12012,11530],{"class":1051},[826,12014,1282],{"class":1178},[826,12016,11535],{"class":1285},[826,12018,1289],{"class":1732},[826,12020,9303],{"class":1051},[826,12022,9308],{"class":1732},[826,12024,11544],{"class":1178},[826,12026,11932],{"class":1051},[826,12028,2380],{"class":1732},[826,12030,9435],{"class":1573},[826,12032,11553],{"class":1732},[826,12034,2182],{"class":1178},[826,12036,12037],{"class":1051}," time\n",[826,12039,12040,12043,12045],{"class":1047,"line":2003},[826,12041,12042],{"class":1178},"        }",[826,12044,1150],{"class":1732},[826,12046,9573],{"class":1178},[826,12048,12049],{"class":1047,"line":2028},[826,12050,12051],{"class":1178},"      }\n",[826,12053,12054],{"class":1047,"line":2038},[826,12055,3918],{"class":1178},[826,12057,12058],{"class":1047,"line":2061},[826,12059,1547],{"emptyLinePlaceholder":877},[826,12061,12062],{"class":1047,"line":2071},[826,12063,12064],{"class":1169},"    // --- BUILD JSON ---\n",[826,12066,12067,12069,12071,12073],{"class":1047,"line":2079},[826,12068,11465],{"class":1592},[826,12070,2377],{"class":1051},[826,12072,1763],{"class":1178},[826,12074,3815],{"class":1178},[826,12076,12077,12080,12082,12084],{"class":1047,"line":2084},[826,12078,12079],{"class":1732},"      project",[826,12081,2182],{"class":1178},[826,12083,7023],{"class":1051},[826,12085,2159],{"class":1178},[826,12087,12088,12091,12093,12095],{"class":1047,"line":2118},[826,12089,12090],{"class":1732},"      bpm",[826,12092,2182],{"class":1178},[826,12094,9357],{"class":1051},[826,12096,2159],{"class":1178},[826,12098,12099,12102,12104],{"class":1047,"line":2136},[826,12100,12101],{"class":1732},"      sections",[826,12103,2182],{"class":1178},[826,12105,12106],{"class":1051}," sections\n",[826,12108,12109],{"class":1047,"line":2162},[826,12110,12111],{"class":1178},"    };\n",[826,12113,12114],{"class":1047,"line":2171},[826,12115,1547],{"emptyLinePlaceholder":877},[826,12117,12118,12120,12123,12125,12127,12129,12131,12133,12135,12137,12140,12143,12145],{"class":1047,"line":2194},[826,12119,11465],{"class":1592},[826,12121,12122],{"class":1051}," json",[826,12124,1763],{"class":1178},[826,12126,9440],{"class":1051},[826,12128,1282],{"class":1178},[826,12130,9445],{"class":1285},[826,12132,1289],{"class":1732},[826,12134,6623],{"class":1051},[826,12136,1299],{"class":1178},[826,12138,12139],{"class":1178}," null,",[826,12141,12142],{"class":1573}," 2",[826,12144,1150],{"class":1732},[826,12146,9573],{"class":1178},[826,12148,12149],{"class":1047,"line":2214},[826,12150,1547],{"emptyLinePlaceholder":877},[826,12152,12153],{"class":1047,"line":2233},[826,12154,12155],{"class":1169},"    // Send to UI (textedit)\n",[826,12157,12158,12161,12163,12165,12167,12169,12171,12173,12175,12177,12179],{"class":1047,"line":2254},[826,12159,12160],{"class":1285},"    outlet",[826,12162,1289],{"class":1732},[826,12164,9435],{"class":1573},[826,12166,1299],{"class":1178},[826,12168,1557],{"class":1178},[826,12170,2460],{"class":1182},[826,12172,1292],{"class":1178},[826,12174,1299],{"class":1178},[826,12176,12122],{"class":1051},[826,12178,1150],{"class":1732},[826,12180,9573],{"class":1178},[826,12182,12183],{"class":1047,"line":2260},[826,12184,1547],{"emptyLinePlaceholder":877},[826,12186,12187],{"class":1047,"line":2268},[826,12188,12189],{"class":1169},"    // Log to Max Console\n",[826,12191,12192,12195,12197,12199,12202,12205,12208,12210,12212],{"class":1047,"line":2300},[826,12193,12194],{"class":1285},"    post",[826,12196,1289],{"class":1732},[826,12198,1292],{"class":1178},[826,12200,12201],{"class":1182},"EXPORT SUCCESS",[826,12203,12204],{"class":1051},"\\\\",[826,12206,12207],{"class":1182},"n",[826,12209,1292],{"class":1178},[826,12211,1150],{"class":1732},[826,12213,9573],{"class":1178},[826,12215,12216],{"class":1047,"line":2321},[826,12217,1547],{"emptyLinePlaceholder":877},[826,12219,12220,12222,12225,12227,12230,12232],{"class":1047,"line":2326},[826,12221,9758],{"class":1178},[826,12223,12224],{"class":1499}," catch",[826,12226,2732],{"class":1732},[826,12228,12229],{"class":1051},"e",[826,12231,9308],{"class":1732},[826,12233,8480],{"class":1178},[826,12235,12236,12238,12240,12242,12245,12247,12249,12252,12254,12257,12259,12261,12263,12265,12267,12269,12271],{"class":1047,"line":2332},[826,12237,12194],{"class":1285},[826,12239,1289],{"class":1732},[826,12241,1292],{"class":1178},[826,12243,12244],{"class":1182},"EXPORT ERROR:",[826,12246,1292],{"class":1178},[826,12248,1299],{"class":1178},[826,12250,12251],{"class":1051}," e",[826,12253,1282],{"class":1178},[826,12255,12256],{"class":1285},"toString",[826,12258,9016],{"class":1732},[826,12260,1299],{"class":1178},[826,12262,1557],{"class":1178},[826,12264,12204],{"class":1051},[826,12266,12207],{"class":1182},[826,12268,1292],{"class":1178},[826,12270,1150],{"class":1732},[826,12272,9573],{"class":1178},[826,12274,12275],{"class":1047,"line":2346},[826,12276,9416],{"class":1178},[826,12278,12279],{"class":1047,"line":2364},[826,12280,3495],{"class":1178},[1482,12282,12284],{"id":12283},"key-concepts-explained","Key Concepts Explained",[746,12286,12287,12290],{},[970,12288,12289],{},"autowatch = 1"," - Tells Max to reload the script automatically when the file changes. Essential during development.",[746,12292,12293,12296],{},[970,12294,12295],{},"LiveAPI"," - The bridge between JavaScript and Ableton's Live Object Model. We use it to access:",[834,12298,12299,12304,12309],{},[837,12300,12301,12303],{},[1043,12302,9043],{}," - The current project",[837,12305,12306,12308],{},[1043,12307,9099],{}," - Current BPM",[837,12310,12311,12313],{},[1043,12312,9183],{}," - Array of locator IDs",[746,12315,12316,12319,12320,1282],{},[970,12317,12318],{},"Array handling"," - LiveAPI often returns single values wrapped in arrays, so we consistently check with ",[1043,12321,9487],{},[746,12323,12324,12327,12328,12330],{},[970,12325,12326],{},"outlet(0, \"set\", json)"," - The \"set\" message tells ",[1043,12329,11209],{}," to display the text without triggering its output.",[1482,12332,12334],{"id":12333},"viewing-the-output","Viewing the Output",[746,12336,12337],{},"The JSON output appears in the Max Console window:",[8248,12339,10375,12340],{"style":11217},[11219,12341,11222,12342,11222,12347,10375],{"style":11221},[8263,12343],{"src":12344,"alt":12345,"style":12346},"/images/blog/musictechlab_blog_ableton-locators-json-console.webp","Max Console showing JSON export output","max-width: 480px; width: 100%; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: block;",[11229,12348,12349],{"style":11231},"Max Console displaying the exported JSON with BPM and sections",[753,12351,12353],{"id":12352},"troubleshooting-common-issues","Troubleshooting Common Issues",[1482,12355,12357],{"id":12356},"js-cant-find-file-marker_exportjs","\"js: can't find file marker_export.js\"",[746,12359,12360],{},"Max can't locate your JavaScript file.",[746,12362,12363],{},[970,12364,12365],{},"Solution:",[1080,12367,12368,12375,12378,12385],{},[837,12369,12370,12371,12374],{},"Right-click on the ",[1043,12372,12373],{},"js"," object in Max",[837,12376,12377],{},"Select \"Open marker_export.js\"",[837,12379,12380,12381,12384],{},"Save As... to the same folder as your ",[1043,12382,12383],{},".amxd"," file",[837,12386,12387,12388,12390],{},"Ensure the object text is just ",[1043,12389,11203],{}," (no paths)",[1482,12392,12394],{"id":12393},"js-no-function-msg_int","\"js: no function msg_int\"",[746,12396,12397],{},"The button sends an integer, but your script doesn't have a handler for it.",[746,12399,12400,12402],{},[970,12401,12365],{}," Add this function to your script:",[1037,12404,12406],{"className":8992,"code":12405,"language":8994,"meta":865,"style":865},"function msg_int(v) {\n  if (v === 1) bang();\n}\n",[1043,12407,12408,12422,12442],{"__ignoreMap":865},[826,12409,12410,12412,12414,12416,12418,12420],{"class":1047,"line":1048},[826,12411,9010],{"class":1592},[826,12413,11404],{"class":1285},[826,12415,1289],{"class":1178},[826,12417,11409],{"class":1302},[826,12419,1150],{"class":1178},[826,12421,3815],{"class":1178},[826,12423,12424,12426,12428,12430,12432,12434,12436,12438,12440],{"class":1047,"line":866},[826,12425,9800],{"class":1499},[826,12427,2732],{"class":1732},[826,12429,11409],{"class":1051},[826,12431,11424],{"class":1178},[826,12433,5797],{"class":1573},[826,12435,9308],{"class":1732},[826,12437,11431],{"class":1285},[826,12439,9016],{"class":1732},[826,12441,9573],{"class":1178},[826,12443,12444],{"class":1047,"line":1060},[826,12445,3495],{"class":1178},[1482,12447,12449],{"id":12448},"song-object-has-no-attribute-locators","\"Song object has no attribute 'locators'\"",[746,12451,12452],{},"The LiveAPI path varies between Ableton versions.",[746,12454,12455,12457,12458,12460],{},[970,12456,12365],{}," Use ",[1043,12459,9183],{}," instead:",[1037,12462,12464],{"className":8992,"code":12463,"language":8994,"meta":865,"style":865},"var locatorIds = song.get(\"cue_points\");\n",[1043,12465,12466],{"__ignoreMap":865},[826,12467,12468,12470,12473,12475,12477,12479,12481,12483,12485,12487,12489,12491],{"class":1047,"line":1048},[826,12469,11792],{"class":1592},[826,12471,12472],{"class":1051}," locatorIds ",[826,12474,1179],{"class":1178},[826,12476,9061],{"class":1051},[826,12478,1282],{"class":1178},[826,12480,2051],{"class":1285},[826,12482,1289],{"class":1051},[826,12484,1292],{"class":1178},[826,12486,9183],{"class":1182},[826,12488,1292],{"class":1178},[826,12490,1150],{"class":1051},[826,12492,9573],{"class":1178},[1482,12494,12496],{"id":12495},"sendmessage-error-2-bad-parameter-value","\"SendMessage error 2: Bad parameter value\"",[8248,12498,10375,12499],{"style":11217},[11219,12500,11222,12501,11222,12506,10375],{"style":11221},[8263,12502],{"src":12503,"alt":12504,"style":12505},"/images/blog/musictechlab_blog_ableton-locators-json-error.webp","SendMessage error in Max Console","max-width: 400px; width: 100%; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: block;",[11229,12507,12508],{"style":11231},"Typical SendMessage errors when LiveAPI path is incorrect",[746,12510,12511],{},"Usually caused by:",[834,12513,12514,12517,12520],{},[837,12515,12516],{},"Invalid LiveAPI path",[837,12518,12519],{},"Querying a property that doesn't exist",[837,12521,12522],{},"Non-numeric values in the locator ID array",[746,12524,12525,12527],{},[970,12526,12365],{}," Filter IDs properly:",[1037,12529,12531],{"className":8992,"code":12530,"language":8994,"meta":865,"style":865},"if (typeof locatorIds[i] !== \"number\") continue;\n",[1043,12532,12533],{"__ignoreMap":865},[826,12534,12535,12537,12539,12541,12544,12546,12548,12550,12552,12554,12556],{"class":1047,"line":1048},[826,12536,5673],{"class":1499},[826,12538,2732],{"class":1051},[826,12540,11834],{"class":1178},[826,12542,12543],{"class":1051}," locatorIds[i] ",[826,12545,11846],{"class":1178},[826,12547,1557],{"class":1178},[826,12549,11851],{"class":1182},[826,12551,1292],{"class":1178},[826,12553,9308],{"class":1051},[826,12555,11858],{"class":1499},[826,12557,9573],{"class":1178},[753,12559,12561],{"id":12560},"adding-file-export-optional","Adding File Export (Optional)",[746,12563,12564],{},"To save the JSON to disk, extend the script:",[1037,12566,12568],{"className":8992,"code":12567,"language":8994,"meta":865,"style":865},"// --- FILE SAVE ---\nvar folder = \"~/Documents/AbletonExports/\";\nvar f = new File(folder + name + \".json\", \"write\");\n\nif (f.isopen) {\n  f.writestring(json);\n  f.close();\n  post(\"Saved to:\", folder + name + \".json\", \"\\\\n\");\n} else {\n  post(\"EXPORT ERROR: cannot write file\\\\n\");\n}\n",[1043,12569,12570,12575,12593,12638,12642,12656,12674,12687,12732,12740,12761],{"__ignoreMap":865},[826,12571,12572],{"class":1047,"line":1048},[826,12573,12574],{"class":1169},"// --- FILE SAVE ---\n",[826,12576,12577,12579,12582,12584,12586,12589,12591],{"class":1047,"line":866},[826,12578,11792],{"class":1592},[826,12580,12581],{"class":1051}," folder ",[826,12583,1179],{"class":1178},[826,12585,1557],{"class":1178},[826,12587,12588],{"class":1182},"~/Documents/AbletonExports/",[826,12590,1292],{"class":1178},[826,12592,9573],{"class":1178},[826,12594,12595,12597,12600,12602,12604,12607,12610,12613,12616,12618,12620,12623,12625,12627,12629,12632,12634,12636],{"class":1047,"line":1060},[826,12596,11792],{"class":1592},[826,12598,12599],{"class":1051}," f ",[826,12601,1179],{"class":1178},[826,12603,9033],{"class":1178},[826,12605,12606],{"class":1285}," File",[826,12608,12609],{"class":1051},"(folder ",[826,12611,12612],{"class":1178},"+",[826,12614,12615],{"class":1051}," name ",[826,12617,12612],{"class":1178},[826,12619,1557],{"class":1178},[826,12621,12622],{"class":1182},".json",[826,12624,1292],{"class":1178},[826,12626,1299],{"class":1178},[826,12628,1557],{"class":1178},[826,12630,12631],{"class":1182},"write",[826,12633,1292],{"class":1178},[826,12635,1150],{"class":1051},[826,12637,9573],{"class":1178},[826,12639,12640],{"class":1047,"line":1066},[826,12641,1547],{"emptyLinePlaceholder":877},[826,12643,12644,12646,12649,12651,12654],{"class":1047,"line":1072},[826,12645,5673],{"class":1499},[826,12647,12648],{"class":1051}," (f",[826,12650,1282],{"class":1178},[826,12652,12653],{"class":1051},"isopen) ",[826,12655,8480],{"class":1178},[826,12657,12658,12661,12663,12666,12668,12670,12672],{"class":1047,"line":1218},[826,12659,12660],{"class":1051},"  f",[826,12662,1282],{"class":1178},[826,12664,12665],{"class":1285},"writestring",[826,12667,1289],{"class":1732},[826,12669,2359],{"class":1051},[826,12671,1150],{"class":1732},[826,12673,9573],{"class":1178},[826,12675,12676,12678,12680,12683,12685],{"class":1047,"line":1229},[826,12677,12660],{"class":1051},[826,12679,1282],{"class":1178},[826,12681,12682],{"class":1285},"close",[826,12684,9016],{"class":1732},[826,12686,9573],{"class":1178},[826,12688,12689,12692,12694,12696,12699,12701,12703,12706,12708,12710,12712,12714,12716,12718,12720,12722,12724,12726,12728,12730],{"class":1047,"line":1584},[826,12690,12691],{"class":1285},"  post",[826,12693,1289],{"class":1732},[826,12695,1292],{"class":1178},[826,12697,12698],{"class":1182},"Saved to:",[826,12700,1292],{"class":1178},[826,12702,1299],{"class":1178},[826,12704,12705],{"class":1051}," folder",[826,12707,9236],{"class":1178},[826,12709,7023],{"class":1051},[826,12711,9236],{"class":1178},[826,12713,1557],{"class":1178},[826,12715,12622],{"class":1182},[826,12717,1292],{"class":1178},[826,12719,1299],{"class":1178},[826,12721,1557],{"class":1178},[826,12723,12204],{"class":1051},[826,12725,12207],{"class":1182},[826,12727,1292],{"class":1178},[826,12729,1150],{"class":1732},[826,12731,9573],{"class":1178},[826,12733,12734,12736,12738],{"class":1047,"line":1589},[826,12735,2153],{"class":1178},[826,12737,9851],{"class":1499},[826,12739,3815],{"class":1178},[826,12741,12742,12744,12746,12748,12751,12753,12755,12757,12759],{"class":1047,"line":1603},[826,12743,12691],{"class":1285},[826,12745,1289],{"class":1732},[826,12747,1292],{"class":1178},[826,12749,12750],{"class":1182},"EXPORT ERROR: cannot write file",[826,12752,12204],{"class":1051},[826,12754,12207],{"class":1182},[826,12756,1292],{"class":1178},[826,12758,1150],{"class":1732},[826,12760,9573],{"class":1178},[826,12762,12763],{"class":1047,"line":1615},[826,12764,3495],{"class":1178},[746,12766,12767,12770,12771,12774],{},[970,12768,12769],{},"Note:"," The ",[1043,12772,12773],{},"~"," path expansion can be inconsistent in Max/JS. For reliability, either:",[834,12776,12777,12780,12783],{},[837,12778,12779],{},"Use absolute paths",[837,12781,12782],{},"Save to the project folder",[837,12784,12785],{},"Create the target directory manually first",[753,12787,12789],{"id":12788},"packaging-for-distribution","Packaging for Distribution",[1482,12791,12793],{"id":12792},"the-simple-way-recommended-for-mvp","The Simple Way (Recommended for MVP)",[746,12795,12796],{},"Create a folder with both files:",[1037,12798,12801],{"className":12799,"code":12800,"language":4882},[4880],"MTL_LocatorsToJSON/\n├── MTL_LocatorsToJSON.amxd\n├── marker_export.js\n└── README.txt\n",[1043,12802,12800],{"__ignoreMap":865},[746,12804,12805],{},"Compress to ZIP and share. Recipients should:",[1080,12807,12808,12811,12817],{},[837,12809,12810],{},"Unzip the folder",[837,12812,12813,12814],{},"Copy to: ",[1043,12815,12816],{},"~/Music/Ableton/User Library/Presets/MIDI Effects/Max MIDI Effect/",[837,12818,12819],{},"Restart Ableton Live (or refresh the Browser)",[1482,12821,12823],{"id":12822},"why-not-alp-ableton-pack","Why Not .alp (Ableton Pack)?",[8248,12825,10375,12826],{"style":11217},[11219,12827,11222,12828,11222,12833,10375],{"style":11221},[8263,12829],{"src":12830,"alt":12831,"style":12832},"/images/blog/musictechlab_blog_ableton-locators-json-pack-error.webp","Ableton Pack error - This is not a valid Pack","max-width: 500px; width: 100%; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: block;",[11229,12834,12835],{"style":11231},"\"This is not a valid Pack\" - why we use ZIP instead of .alp",[746,12837,1243,12838,12841,12842,12845],{},[1043,12839,12840],{},".alp"," format is ",[970,12843,12844],{},"not"," a simple ZIP with a different extension. Creating valid Packs requires Ableton's specific export workflow, which varies between Live versions. For quick distribution, a ZIP folder is more reliable.",[1482,12847,12849],{"id":12848},"project-folder-structure","Project Folder Structure",[8248,12851,10375,12852],{"style":11217},[11219,12853,11222,12854,11222,12858,10375],{"style":11221},[8263,12855],{"src":12856,"alt":12857,"style":12505},"/images/blog/musictechlab_blog_ableton-locators-json-folder.webp","Finder showing the project folder structure",[11229,12859,12860],{"style":11231},"Project folder with the .amxd device and JS file",[753,12862,12864],{"id":12863},"usage-instructions","Usage Instructions",[8248,12866,10375,12867],{"style":11217},[11219,12868,11222,12869,11222,12874,10375],{"style":11221},[8263,12870],{"src":12871,"alt":12872,"style":12873},"/images/blog/musictechlab_blog_ableton-locators-json-browser.webp","Device visible in Ableton Live browser","max-width: 360px; width: 100%; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: block;",[11229,12875,12876],{"style":11231},"The device appears in Ableton's Browser under Max MIDI Effect",[1080,12878,12879,12885,12891,12897,12902],{},[837,12880,12881,12884],{},[970,12882,12883],{},"Add the device"," to any MIDI track",[837,12886,12887,12890],{},[970,12888,12889],{},"Create locators"," in the Arrangement view (right-click → Add Locator, or Ctrl/Cmd+Shift+L)",[837,12892,12893,12896],{},[970,12894,12895],{},"Name your locators"," (INTRO, VERSE, CHORUS, etc.)",[837,12898,12899],{},[970,12900,12901],{},"Click EXPORT JSON",[837,12903,12904,12907],{},[970,12905,12906],{},"Copy the JSON"," from the text display or find it in your export folder",[753,12909,12911],{"id":12910},"integration-with-ai-workflows","Integration with AI Workflows",[746,12913,12914,12915,12918],{},"This JSON format is designed to be compatible with our ",[922,12916,12917],{"href":101},"song structure analysis pipeline",". You can:",[1080,12920,12921,12926,12932,12938],{},[837,12922,12923,12925],{},[970,12924,10016],{}," manually-annotated structures from Ableton",[837,12927,12928,12931],{},[970,12929,12930],{},"Compare"," with AI-detected structures",[837,12933,12934,12937],{},[970,12935,12936],{},"Train"," custom models on your annotations",[837,12939,12940,12943],{},[970,12941,12942],{},"Import"," AI-generated structures back into your DAW",[753,12945,10705],{"id":10704},[746,12947,12948],{},"This MVP opens several possibilities:",[946,12950,12951,12960],{},[949,12952,12953],{},[952,12954,12955,12958],{},[955,12956,12957],{},"Enhancement",[955,12959,10312],{},[962,12961,12962,12972,12982,12992,13002],{},[952,12963,12964,12969],{},[967,12965,12966],{},[970,12967,12968],{},"Bidirectional sync",[967,12970,12971],{},"Import JSON to create locators",[952,12973,12974,12979],{},[967,12975,12976],{},[970,12977,12978],{},"Audio Effect version",[967,12980,12981],{},"Support audio tracks, not just MIDI",[952,12983,12984,12989],{},[967,12985,12986],{},[970,12987,12988],{},"Real-time export",[967,12990,12991],{},"Auto-export on locator changes",[952,12993,12994,12999],{},[967,12995,12996],{},[970,12997,12998],{},"Cloud sync",[967,13000,13001],{},"Push to API endpoint directly",[952,13003,13004,13009],{},[967,13005,13006],{},[970,13007,13008],{},"Time signature support",[967,13010,13011],{},"Include meter changes",[753,13013,13015],{"id":13014},"complete-code-reference","Complete Code Reference",[746,13017,13018],{},"The final working script:",[1037,13020,13022],{"className":8992,"code":13021,"language":8994,"meta":865,"style":865},"autowatch = 1;\noutlets = 1;\n\nfunction msg_int(v) {\n  if (v === 1) bang();\n}\n\nfunction bang() {\n  try {\n    var song = new LiveAPI(\"live_set\");\n\n    // PROJECT NAME\n    var name = song.get(\"name\");\n    name = Array.isArray(name) ? name[0] : name;\n    if (!name || name === \"\") name = \"untitled\";\n    name = name.replace(/[^a-z0-9_\\-]/gi, \"_\");\n\n    // BPM\n    var bpm = song.get(\"tempo\");\n    bpm = Array.isArray(bpm) ? bpm[0] : bpm;\n\n    // LOCATORS\n    var locatorIds = song.get(\"cue_points\");\n    var sections = [];\n\n    if (Array.isArray(locatorIds)) {\n      for (var i = 0; i \u003C locatorIds.length; i++) {\n        if (typeof locatorIds[i] !== \"number\") continue;\n\n        var l = new LiveAPI(\"id \" + locatorIds[i]);\n        var label = l.get(\"name\");\n        var time = l.get(\"time\");\n\n        sections.push({\n          label: Array.isArray(label) ? label[0] : label,\n          time_seconds: Array.isArray(time) ? time[0] : time\n        });\n      }\n    }\n\n    // JSON OUTPUT\n    var data = {\n      project: name,\n      bpm: bpm,\n      sections: sections\n    };\n\n    var json = JSON.stringify(data, null, 2);\n    outlet(0, \"set\", json);\n\n    // OPTIONAL: File save\n    var folder = \"~/Documents/AbletonExports/\";\n    var f = new File(folder + name + \".json\", \"write\");\n    if (f.isopen) {\n      f.writestring(json);\n      f.close();\n    }\n\n  } catch (e) {\n    post(\"EXPORT ERROR:\", e.toString(), \"\\\\n\");\n  }\n}\n",[1043,13023,13024,13034,13044,13048,13062,13082,13086,13090,13100,13106,13130,13134,13139,13165,13199,13231,13267,13271,13276,13302,13336,13340,13345,13371,13383,13387,13407,13443,13473,13477,13509,13535,13561,13565,13577,13611,13643,13651,13655,13659,13663,13668,13678,13688,13698,13706,13710,13714,13742,13766,13770,13775,13791,13832,13850,13867,13879,13883,13887,13901,13937,13941],{"__ignoreMap":865},[826,13025,13026,13028,13030,13032],{"class":1047,"line":1048},[826,13027,11376],{"class":1051},[826,13029,1179],{"class":1178},[826,13031,5797],{"class":1573},[826,13033,9573],{"class":1178},[826,13035,13036,13038,13040,13042],{"class":1047,"line":866},[826,13037,11387],{"class":1051},[826,13039,1179],{"class":1178},[826,13041,5797],{"class":1573},[826,13043,9573],{"class":1178},[826,13045,13046],{"class":1047,"line":1060},[826,13047,1547],{"emptyLinePlaceholder":877},[826,13049,13050,13052,13054,13056,13058,13060],{"class":1047,"line":1066},[826,13051,9010],{"class":1592},[826,13053,11404],{"class":1285},[826,13055,1289],{"class":1178},[826,13057,11409],{"class":1302},[826,13059,1150],{"class":1178},[826,13061,3815],{"class":1178},[826,13063,13064,13066,13068,13070,13072,13074,13076,13078,13080],{"class":1047,"line":1072},[826,13065,9800],{"class":1499},[826,13067,2732],{"class":1732},[826,13069,11409],{"class":1051},[826,13071,11424],{"class":1178},[826,13073,5797],{"class":1573},[826,13075,9308],{"class":1732},[826,13077,11431],{"class":1285},[826,13079,9016],{"class":1732},[826,13081,9573],{"class":1178},[826,13083,13084],{"class":1047,"line":1218},[826,13085,3495],{"class":1178},[826,13087,13088],{"class":1047,"line":1229},[826,13089,1547],{"emptyLinePlaceholder":877},[826,13091,13092,13094,13096,13098],{"class":1047,"line":1584},[826,13093,9010],{"class":1592},[826,13095,9013],{"class":1285},[826,13097,9016],{"class":1178},[826,13099,3815],{"class":1178},[826,13101,13102,13104],{"class":1047,"line":1589},[826,13103,11458],{"class":1499},[826,13105,3815],{"class":1178},[826,13107,13108,13110,13112,13114,13116,13118,13120,13122,13124,13126,13128],{"class":1047,"line":1603},[826,13109,11465],{"class":1592},[826,13111,9061],{"class":1051},[826,13113,1763],{"class":1178},[826,13115,9033],{"class":1178},[826,13117,9036],{"class":1285},[826,13119,1289],{"class":1732},[826,13121,1292],{"class":1178},[826,13123,9043],{"class":1182},[826,13125,1292],{"class":1178},[826,13127,1150],{"class":1732},[826,13129,9573],{"class":1178},[826,13131,13132],{"class":1047,"line":1615},[826,13133,1547],{"emptyLinePlaceholder":877},[826,13135,13136],{"class":1047,"line":1620},[826,13137,13138],{"class":1169},"    // PROJECT NAME\n",[826,13140,13141,13143,13145,13147,13149,13151,13153,13155,13157,13159,13161,13163],{"class":1047,"line":1631},[826,13142,11465],{"class":1592},[826,13144,7023],{"class":1051},[826,13146,1763],{"class":1178},[826,13148,9061],{"class":1051},[826,13150,1282],{"class":1178},[826,13152,2051],{"class":1285},[826,13154,1289],{"class":1732},[826,13156,1292],{"class":1178},[826,13158,5322],{"class":1182},[826,13160,1292],{"class":1178},[826,13162,1150],{"class":1732},[826,13164,9573],{"class":1178},[826,13166,13167,13169,13171,13173,13175,13177,13179,13181,13183,13185,13187,13189,13191,13193,13195,13197],{"class":1047,"line":1642},[826,13168,11525],{"class":1051},[826,13170,1763],{"class":1178},[826,13172,11530],{"class":1051},[826,13174,1282],{"class":1178},[826,13176,11535],{"class":1285},[826,13178,1289],{"class":1732},[826,13180,5322],{"class":1051},[826,13182,9308],{"class":1732},[826,13184,11544],{"class":1178},[826,13186,7023],{"class":1051},[826,13188,2380],{"class":1732},[826,13190,9435],{"class":1573},[826,13192,11553],{"class":1732},[826,13194,2182],{"class":1178},[826,13196,7023],{"class":1051},[826,13198,9573],{"class":1178},[826,13200,13201,13203,13205,13207,13209,13211,13213,13215,13217,13219,13221,13223,13225,13227,13229],{"class":1047,"line":1652},[826,13202,2608],{"class":1499},[826,13204,2732],{"class":1732},[826,13206,11568],{"class":1178},[826,13208,5322],{"class":1051},[826,13210,11573],{"class":1178},[826,13212,7023],{"class":1051},[826,13214,11424],{"class":1178},[826,13216,1784],{"class":1178},[826,13218,9308],{"class":1732},[826,13220,5322],{"class":1051},[826,13222,1763],{"class":1178},[826,13224,1557],{"class":1178},[826,13226,11590],{"class":1182},[826,13228,1292],{"class":1178},[826,13230,9573],{"class":1178},[826,13232,13233,13235,13237,13239,13241,13243,13245,13247,13249,13251,13253,13255,13257,13259,13261,13263,13265],{"class":1047,"line":1662},[826,13234,11525],{"class":1051},[826,13236,1763],{"class":1178},[826,13238,7023],{"class":1051},[826,13240,1282],{"class":1178},[826,13242,11607],{"class":1285},[826,13244,1289],{"class":1732},[826,13246,11612],{"class":1178},[826,13248,11615],{"class":1182},[826,13250,11618],{"class":1051},[826,13252,11621],{"class":1178},[826,13254,11624],{"class":1573},[826,13256,1299],{"class":1178},[826,13258,1557],{"class":1178},[826,13260,11631],{"class":1182},[826,13262,1292],{"class":1178},[826,13264,1150],{"class":1732},[826,13266,9573],{"class":1178},[826,13268,13269],{"class":1047,"line":1672},[826,13270,1547],{"emptyLinePlaceholder":877},[826,13272,13273],{"class":1047,"line":1677},[826,13274,13275],{"class":1169},"    // BPM\n",[826,13277,13278,13280,13282,13284,13286,13288,13290,13292,13294,13296,13298,13300],{"class":1047,"line":1686},[826,13279,11465],{"class":1592},[826,13281,9357],{"class":1051},[826,13283,1763],{"class":1178},[826,13285,9061],{"class":1051},[826,13287,1282],{"class":1178},[826,13289,2051],{"class":1285},[826,13291,1289],{"class":1732},[826,13293,1292],{"class":1178},[826,13295,9099],{"class":1182},[826,13297,1292],{"class":1178},[826,13299,1150],{"class":1732},[826,13301,9573],{"class":1178},[826,13303,13304,13306,13308,13310,13312,13314,13316,13318,13320,13322,13324,13326,13328,13330,13332,13334],{"class":1047,"line":1710},[826,13305,9352],{"class":1051},[826,13307,1763],{"class":1178},[826,13309,11530],{"class":1051},[826,13311,1282],{"class":1178},[826,13313,11535],{"class":1285},[826,13315,1289],{"class":1732},[826,13317,8508],{"class":1051},[826,13319,9308],{"class":1732},[826,13321,11544],{"class":1178},[826,13323,9357],{"class":1051},[826,13325,2380],{"class":1732},[826,13327,9435],{"class":1573},[826,13329,11553],{"class":1732},[826,13331,2182],{"class":1178},[826,13333,9357],{"class":1051},[826,13335,9573],{"class":1178},[826,13337,13338],{"class":1047,"line":1721},[826,13339,1547],{"emptyLinePlaceholder":877},[826,13341,13342],{"class":1047,"line":1738},[826,13343,13344],{"class":1169},"    // LOCATORS\n",[826,13346,13347,13349,13351,13353,13355,13357,13359,13361,13363,13365,13367,13369],{"class":1047,"line":1747},[826,13348,11465],{"class":1592},[826,13350,11722],{"class":1051},[826,13352,1763],{"class":1178},[826,13354,9061],{"class":1051},[826,13356,1282],{"class":1178},[826,13358,2051],{"class":1285},[826,13360,1289],{"class":1732},[826,13362,1292],{"class":1178},[826,13364,9183],{"class":1182},[826,13366,1292],{"class":1178},[826,13368,1150],{"class":1732},[826,13370,9573],{"class":1178},[826,13372,13373,13375,13377,13379,13381],{"class":1047,"line":1752},[826,13374,11465],{"class":1592},[826,13376,9397],{"class":1051},[826,13378,1763],{"class":1178},[826,13380,11753],{"class":1732},[826,13382,9573],{"class":1178},[826,13384,13385],{"class":1047,"line":1789},[826,13386,1547],{"emptyLinePlaceholder":877},[826,13388,13389,13391,13393,13395,13397,13399,13401,13403,13405],{"class":1047,"line":1821},[826,13390,2608],{"class":1499},[826,13392,2732],{"class":1732},[826,13394,11768],{"class":1051},[826,13396,1282],{"class":1178},[826,13398,11535],{"class":1285},[826,13400,1289],{"class":1732},[826,13402,11777],{"class":1051},[826,13404,11780],{"class":1732},[826,13406,8480],{"class":1178},[826,13408,13409,13411,13413,13415,13417,13419,13421,13423,13425,13427,13429,13431,13433,13435,13437,13439,13441],{"class":1047,"line":1853},[826,13410,11787],{"class":1499},[826,13412,2732],{"class":1732},[826,13414,11792],{"class":1592},[826,13416,11795],{"class":1051},[826,13418,1763],{"class":1178},[826,13420,5006],{"class":1573},[826,13422,11802],{"class":1178},[826,13424,11795],{"class":1051},[826,13426,11807],{"class":1178},[826,13428,11722],{"class":1051},[826,13430,1282],{"class":1178},[826,13432,11814],{"class":1051},[826,13434,11802],{"class":1178},[826,13436,11795],{"class":1051},[826,13438,11821],{"class":1178},[826,13440,9308],{"class":1732},[826,13442,8480],{"class":1178},[826,13444,13445,13447,13449,13451,13453,13455,13457,13459,13461,13463,13465,13467,13469,13471],{"class":1047,"line":1885},[826,13446,1724],{"class":1499},[826,13448,2732],{"class":1732},[826,13450,11834],{"class":1178},[826,13452,11722],{"class":1051},[826,13454,2380],{"class":1732},[826,13456,11841],{"class":1051},[826,13458,11553],{"class":1732},[826,13460,11846],{"class":1178},[826,13462,1557],{"class":1178},[826,13464,11851],{"class":1182},[826,13466,1292],{"class":1178},[826,13468,9308],{"class":1732},[826,13470,11858],{"class":1499},[826,13472,9573],{"class":1178},[826,13474,13475],{"class":1047,"line":1890},[826,13476,1547],{"emptyLinePlaceholder":877},[826,13478,13479,13481,13483,13485,13487,13489,13491,13493,13495,13497,13499,13501,13503,13505,13507],{"class":1047,"line":1937},[826,13480,11869],{"class":1592},[826,13482,11872],{"class":1051},[826,13484,1763],{"class":1178},[826,13486,9033],{"class":1178},[826,13488,9036],{"class":1285},[826,13490,1289],{"class":1732},[826,13492,1292],{"class":1178},[826,13494,9231],{"class":1182},[826,13496,1292],{"class":1178},[826,13498,9236],{"class":1178},[826,13500,11722],{"class":1051},[826,13502,2380],{"class":1732},[826,13504,11841],{"class":1051},[826,13506,2857],{"class":1732},[826,13508,9573],{"class":1178},[826,13510,13511,13513,13515,13517,13519,13521,13523,13525,13527,13529,13531,13533],{"class":1047,"line":1958},[826,13512,11869],{"class":1592},[826,13514,11905],{"class":1051},[826,13516,1763],{"class":1178},[826,13518,11872],{"class":1051},[826,13520,1282],{"class":1178},[826,13522,2051],{"class":1285},[826,13524,1289],{"class":1732},[826,13526,1292],{"class":1178},[826,13528,5322],{"class":1182},[826,13530,1292],{"class":1178},[826,13532,1150],{"class":1732},[826,13534,9573],{"class":1178},[826,13536,13537,13539,13541,13543,13545,13547,13549,13551,13553,13555,13557,13559],{"class":1047,"line":1965},[826,13538,11869],{"class":1592},[826,13540,11932],{"class":1051},[826,13542,1763],{"class":1178},[826,13544,11872],{"class":1051},[826,13546,1282],{"class":1178},[826,13548,2051],{"class":1285},[826,13550,1289],{"class":1732},[826,13552,1292],{"class":1178},[826,13554,9303],{"class":1182},[826,13556,1292],{"class":1178},[826,13558,1150],{"class":1732},[826,13560,9573],{"class":1178},[826,13562,13563],{"class":1047,"line":1970},[826,13564,1547],{"emptyLinePlaceholder":877},[826,13566,13567,13569,13571,13573,13575],{"class":1047,"line":1983},[826,13568,10164],{"class":1051},[826,13570,1282],{"class":1178},[826,13572,9250],{"class":1285},[826,13574,1289],{"class":1732},[826,13576,8480],{"class":1178},[826,13578,13579,13581,13583,13585,13587,13589,13591,13593,13595,13597,13599,13601,13603,13605,13607,13609],{"class":1047,"line":1991},[826,13580,11973],{"class":1732},[826,13582,2182],{"class":1178},[826,13584,11530],{"class":1051},[826,13586,1282],{"class":1178},[826,13588,11535],{"class":1285},[826,13590,1289],{"class":1732},[826,13592,5067],{"class":1051},[826,13594,9308],{"class":1732},[826,13596,11544],{"class":1178},[826,13598,11905],{"class":1051},[826,13600,2380],{"class":1732},[826,13602,9435],{"class":1573},[826,13604,11553],{"class":1732},[826,13606,2182],{"class":1178},[826,13608,11905],{"class":1051},[826,13610,2159],{"class":1178},[826,13612,13613,13615,13617,13619,13621,13623,13625,13627,13629,13631,13633,13635,13637,13639,13641],{"class":1047,"line":1996},[826,13614,12008],{"class":1732},[826,13616,2182],{"class":1178},[826,13618,11530],{"class":1051},[826,13620,1282],{"class":1178},[826,13622,11535],{"class":1285},[826,13624,1289],{"class":1732},[826,13626,9303],{"class":1051},[826,13628,9308],{"class":1732},[826,13630,11544],{"class":1178},[826,13632,11932],{"class":1051},[826,13634,2380],{"class":1732},[826,13636,9435],{"class":1573},[826,13638,11553],{"class":1732},[826,13640,2182],{"class":1178},[826,13642,12037],{"class":1051},[826,13644,13645,13647,13649],{"class":1047,"line":2003},[826,13646,12042],{"class":1178},[826,13648,1150],{"class":1732},[826,13650,9573],{"class":1178},[826,13652,13653],{"class":1047,"line":2028},[826,13654,12051],{"class":1178},[826,13656,13657],{"class":1047,"line":2038},[826,13658,3918],{"class":1178},[826,13660,13661],{"class":1047,"line":2061},[826,13662,1547],{"emptyLinePlaceholder":877},[826,13664,13665],{"class":1047,"line":2071},[826,13666,13667],{"class":1169},"    // JSON OUTPUT\n",[826,13669,13670,13672,13674,13676],{"class":1047,"line":2079},[826,13671,11465],{"class":1592},[826,13673,2377],{"class":1051},[826,13675,1763],{"class":1178},[826,13677,3815],{"class":1178},[826,13679,13680,13682,13684,13686],{"class":1047,"line":2084},[826,13681,12079],{"class":1732},[826,13683,2182],{"class":1178},[826,13685,7023],{"class":1051},[826,13687,2159],{"class":1178},[826,13689,13690,13692,13694,13696],{"class":1047,"line":2118},[826,13691,12090],{"class":1732},[826,13693,2182],{"class":1178},[826,13695,9357],{"class":1051},[826,13697,2159],{"class":1178},[826,13699,13700,13702,13704],{"class":1047,"line":2136},[826,13701,12101],{"class":1732},[826,13703,2182],{"class":1178},[826,13705,12106],{"class":1051},[826,13707,13708],{"class":1047,"line":2162},[826,13709,12111],{"class":1178},[826,13711,13712],{"class":1047,"line":2171},[826,13713,1547],{"emptyLinePlaceholder":877},[826,13715,13716,13718,13720,13722,13724,13726,13728,13730,13732,13734,13736,13738,13740],{"class":1047,"line":2194},[826,13717,11465],{"class":1592},[826,13719,12122],{"class":1051},[826,13721,1763],{"class":1178},[826,13723,9440],{"class":1051},[826,13725,1282],{"class":1178},[826,13727,9445],{"class":1285},[826,13729,1289],{"class":1732},[826,13731,6623],{"class":1051},[826,13733,1299],{"class":1178},[826,13735,12139],{"class":1178},[826,13737,12142],{"class":1573},[826,13739,1150],{"class":1732},[826,13741,9573],{"class":1178},[826,13743,13744,13746,13748,13750,13752,13754,13756,13758,13760,13762,13764],{"class":1047,"line":2214},[826,13745,12160],{"class":1285},[826,13747,1289],{"class":1732},[826,13749,9435],{"class":1573},[826,13751,1299],{"class":1178},[826,13753,1557],{"class":1178},[826,13755,2460],{"class":1182},[826,13757,1292],{"class":1178},[826,13759,1299],{"class":1178},[826,13761,12122],{"class":1051},[826,13763,1150],{"class":1732},[826,13765,9573],{"class":1178},[826,13767,13768],{"class":1047,"line":2233},[826,13769,1547],{"emptyLinePlaceholder":877},[826,13771,13772],{"class":1047,"line":2254},[826,13773,13774],{"class":1169},"    // OPTIONAL: File save\n",[826,13776,13777,13779,13781,13783,13785,13787,13789],{"class":1047,"line":2260},[826,13778,11465],{"class":1592},[826,13780,12705],{"class":1051},[826,13782,1763],{"class":1178},[826,13784,1557],{"class":1178},[826,13786,12588],{"class":1182},[826,13788,1292],{"class":1178},[826,13790,9573],{"class":1178},[826,13792,13793,13795,13797,13799,13801,13803,13805,13808,13810,13812,13814,13816,13818,13820,13822,13824,13826,13828,13830],{"class":1047,"line":2268},[826,13794,11465],{"class":1592},[826,13796,2280],{"class":1051},[826,13798,1763],{"class":1178},[826,13800,9033],{"class":1178},[826,13802,12606],{"class":1285},[826,13804,1289],{"class":1732},[826,13806,13807],{"class":1051},"folder",[826,13809,9236],{"class":1178},[826,13811,7023],{"class":1051},[826,13813,9236],{"class":1178},[826,13815,1557],{"class":1178},[826,13817,12622],{"class":1182},[826,13819,1292],{"class":1178},[826,13821,1299],{"class":1178},[826,13823,1557],{"class":1178},[826,13825,12631],{"class":1182},[826,13827,1292],{"class":1178},[826,13829,1150],{"class":1732},[826,13831,9573],{"class":1178},[826,13833,13834,13836,13838,13841,13843,13846,13848],{"class":1047,"line":2300},[826,13835,2608],{"class":1499},[826,13837,2732],{"class":1732},[826,13839,13840],{"class":1051},"f",[826,13842,1282],{"class":1178},[826,13844,13845],{"class":1051},"isopen",[826,13847,9308],{"class":1732},[826,13849,8480],{"class":1178},[826,13851,13852,13855,13857,13859,13861,13863,13865],{"class":1047,"line":2321},[826,13853,13854],{"class":1051},"      f",[826,13856,1282],{"class":1178},[826,13858,12665],{"class":1285},[826,13860,1289],{"class":1732},[826,13862,2359],{"class":1051},[826,13864,1150],{"class":1732},[826,13866,9573],{"class":1178},[826,13868,13869,13871,13873,13875,13877],{"class":1047,"line":2326},[826,13870,13854],{"class":1051},[826,13872,1282],{"class":1178},[826,13874,12682],{"class":1285},[826,13876,9016],{"class":1732},[826,13878,9573],{"class":1178},[826,13880,13881],{"class":1047,"line":2332},[826,13882,3918],{"class":1178},[826,13884,13885],{"class":1047,"line":2346},[826,13886,1547],{"emptyLinePlaceholder":877},[826,13888,13889,13891,13893,13895,13897,13899],{"class":1047,"line":2364},[826,13890,9758],{"class":1178},[826,13892,12224],{"class":1499},[826,13894,2732],{"class":1732},[826,13896,12229],{"class":1051},[826,13898,9308],{"class":1732},[826,13900,8480],{"class":1178},[826,13902,13903,13905,13907,13909,13911,13913,13915,13917,13919,13921,13923,13925,13927,13929,13931,13933,13935],{"class":1047,"line":2369},[826,13904,12194],{"class":1285},[826,13906,1289],{"class":1732},[826,13908,1292],{"class":1178},[826,13910,12244],{"class":1182},[826,13912,1292],{"class":1178},[826,13914,1299],{"class":1178},[826,13916,12251],{"class":1051},[826,13918,1282],{"class":1178},[826,13920,12256],{"class":1285},[826,13922,9016],{"class":1732},[826,13924,1299],{"class":1178},[826,13926,1557],{"class":1178},[826,13928,12204],{"class":1051},[826,13930,12207],{"class":1182},[826,13932,1292],{"class":1178},[826,13934,1150],{"class":1732},[826,13936,9573],{"class":1178},[826,13938,13939],{"class":1047,"line":2393},[826,13940,9416],{"class":1178},[826,13942,13943],{"class":1047,"line":2423},[826,13944,3495],{"class":1178},[753,13946,10765],{"id":10764},[746,13948,13949],{},"With about 50 lines of JavaScript and a simple Max for Live patcher, we've built a bridge between Ableton Live's arrangement markers and the wider world of data-driven music tools.",[746,13951,13952,13953,13956],{},"This approach demonstrates a key principle: ",[970,13954,13955],{},"start with the simplest thing that works",". A button, a script, and a text display. No complex UI, no elaborate state management. Just structured data flowing from your DAW to wherever it needs to go.",[746,13958,13959],{},"The JSON format we've chosen is intentionally minimal - project name, BPM, and an array of labeled timestamps. This makes it trivial to parse in any language and integrate with any system.",[8170,13961,13962],{},"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 .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 pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}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 .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 .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 .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}",{"title":865,"searchDepth":866,"depth":866,"links":13964},[13965,13966,13967,13968,13973,13977,13983,13984,13989,13990,13991,13992,13993],{"id":10879,"depth":866,"text":10880},{"id":11130,"depth":866,"text":11131},{"id":1031,"depth":866,"text":11181},{"id":11213,"depth":866,"text":11214,"children":13969},[13970,13971,13972],{"id":11235,"depth":1060,"text":11236},{"id":11259,"depth":1060,"text":11260},{"id":11327,"depth":1060,"text":11328},{"id":11362,"depth":866,"text":11363,"children":13974},[13975,13976],{"id":12283,"depth":1060,"text":12284},{"id":12333,"depth":1060,"text":12334},{"id":12352,"depth":866,"text":12353,"children":13978},[13979,13980,13981,13982],{"id":12356,"depth":1060,"text":12357},{"id":12393,"depth":1060,"text":12394},{"id":12448,"depth":1060,"text":12449},{"id":12495,"depth":1060,"text":12496},{"id":12560,"depth":866,"text":12561},{"id":12788,"depth":866,"text":12789,"children":13985},[13986,13987,13988],{"id":12792,"depth":1060,"text":12793},{"id":12822,"depth":1060,"text":12823},{"id":12848,"depth":1060,"text":12849},{"id":12863,"depth":866,"text":12864},{"id":12910,"depth":866,"text":12911},{"id":10704,"depth":866,"text":10705},{"id":13014,"depth":866,"text":13015},{"id":10764,"depth":866,"text":10765},"2026-01-24T00:00:00.000Z","Build a Max for Live device that exports arrangement locators and BPM to JSON, ready for external tools and AI music analysis pipelines.",{"src":13997},"/images/blog/musictechlab_blog_ableton-locators-json-hero.webp",{"enabled":877,"items":13999},[14000,14003,14006,14008],{"text":14001,"icon":14002},"A one-click Max for Live device exports arrangement locators and BPM to JSON.","i-lucide-music",{"text":14004,"icon":14005},"Output feeds into AI models, cross-DAW workflows, and video editing tools.","i-lucide-brain",{"text":14007,"icon":884},"Built with three components: a button, JavaScript using LiveAPI, and a text display.",{"text":14009,"icon":14010},"Requires Ableton Live 12 Suite for Max for Live support.","i-lucide-headphones",{},{"title":474,"description":13995},[10836,894],"znDSVZTcrgfgHaCE3fnvM0ylRVU8K_vjHV1ACi7Y0WY",{"id":14016,"title":454,"authors":14017,"badge":14020,"body":14023,"category":873,"client":741,"date":15178,"description":15179,"extension":876,"faq":741,"featured":69,"featuredOrder":741,"hidden":69,"image":15180,"keyTakeaways":15182,"meta":15191,"navigation":877,"path":455,"seo":15192,"status":741,"stem":456,"tags":15193,"teaser":741,"__hash__":15194,"score":1060},"posts/blog/software-development/did-you-know-dev-tips-part-1.md",[14018],{"name":906,"to":907,"avatar":14019},{"src":909},{"label":14021,"color":14022},"Series","#7C3AED",{"type":743,"value":14024,"toc":15164},[14025,14028,14030,14034,14040,14050,14204,14207,14209,14213,14218,14223,14245,14248,14250,14254,14259,14264,14304,14307,14309,14313,14321,14337,14453,14456,14458,14462,14467,14472,14486,14489,14491,14495,14500,14509,14623,14626,14628,14632,14637,14642,14708,14719,14721,14725,14730,14735,14831,14834,14836,14840,14845,14850,14956,14962,14964,14968,14973,14978,15019,15022,15024,15028,15033,15142,15145,15147,15149,15152,15161],[746,14026,14027],{},"Welcome to \"Did You Know?\" - a series where we share practical tips, clever workarounds, and non-obvious solutions discovered in real production codebases. These aren't theoretical best practices - they're battle-tested patterns from actual projects.",[8270,14029],{},[753,14031,14033],{"id":14032},"_1-codemagic-cant-build-ios-and-macos-simultaneously-but-theres-a-workaround","1. Codemagic Can't Build iOS and macOS Simultaneously (But There's a Workaround)",[746,14035,14036,14039],{},[970,14037,14038],{},"The Problem:"," You have a Flutter app targeting both iOS and macOS. You'd expect to build them in one pipeline, but Codemagic doesn't allow selecting both platforms in a single workflow.",[746,14041,14042,14045,14046,14049],{},[970,14043,14044],{},"Did you know?"," The solution is to use custom ",[1043,14047,14048],{},"codemagic.yaml"," files with separate workflows:",[1037,14051,14055],{"className":14052,"code":14053,"language":14054,"meta":865,"style":865},"language-yaml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","workflows:\n  ios-build:\n    name: iOS Build\n    instance_type: mac_mini_m2\n    environment:\n      xcode: latest\n    scripts:\n      - flutter build ios --release\n    artifacts:\n      - build/ios/ipa/*.ipa\n\n  macos-build:\n    name: macOS Build\n    instance_type: mac_mini_m2\n    environment:\n      xcode: latest\n    scripts:\n      - flutter build macos --release\n    artifacts:\n      - build/macos/**/*.app\n","yaml",[1043,14056,14057,14064,14071,14080,14090,14097,14107,14114,14122,14129,14136,14140,14147,14156,14164,14170,14178,14184,14191,14197],{"__ignoreMap":865},[826,14058,14059,14062],{"class":1047,"line":1048},[826,14060,14061],{"class":1732},"workflows",[826,14063,1600],{"class":1178},[826,14065,14066,14069],{"class":1047,"line":866},[826,14067,14068],{"class":1732},"  ios-build",[826,14070,1600],{"class":1178},[826,14072,14073,14075,14077],{"class":1047,"line":1060},[826,14074,11525],{"class":1732},[826,14076,2182],{"class":1178},[826,14078,14079],{"class":1182}," iOS Build\n",[826,14081,14082,14085,14087],{"class":1047,"line":1066},[826,14083,14084],{"class":1732},"    instance_type",[826,14086,2182],{"class":1178},[826,14088,14089],{"class":1182}," mac_mini_m2\n",[826,14091,14092,14095],{"class":1047,"line":1072},[826,14093,14094],{"class":1732},"    environment",[826,14096,1600],{"class":1178},[826,14098,14099,14102,14104],{"class":1047,"line":1218},[826,14100,14101],{"class":1732},"      xcode",[826,14103,2182],{"class":1178},[826,14105,14106],{"class":1182}," latest\n",[826,14108,14109,14112],{"class":1047,"line":1229},[826,14110,14111],{"class":1732},"    scripts",[826,14113,1600],{"class":1178},[826,14115,14116,14119],{"class":1047,"line":1584},[826,14117,14118],{"class":1178},"      -",[826,14120,14121],{"class":1182}," flutter build ios --release\n",[826,14123,14124,14127],{"class":1047,"line":1589},[826,14125,14126],{"class":1732},"    artifacts",[826,14128,1600],{"class":1178},[826,14130,14131,14133],{"class":1047,"line":1603},[826,14132,14118],{"class":1178},[826,14134,14135],{"class":1182}," build/ios/ipa/*.ipa\n",[826,14137,14138],{"class":1047,"line":1615},[826,14139,1547],{"emptyLinePlaceholder":877},[826,14141,14142,14145],{"class":1047,"line":1620},[826,14143,14144],{"class":1732},"  macos-build",[826,14146,1600],{"class":1178},[826,14148,14149,14151,14153],{"class":1047,"line":1631},[826,14150,11525],{"class":1732},[826,14152,2182],{"class":1178},[826,14154,14155],{"class":1182}," macOS Build\n",[826,14157,14158,14160,14162],{"class":1047,"line":1642},[826,14159,14084],{"class":1732},[826,14161,2182],{"class":1178},[826,14163,14089],{"class":1182},[826,14165,14166,14168],{"class":1047,"line":1652},[826,14167,14094],{"class":1732},[826,14169,1600],{"class":1178},[826,14171,14172,14174,14176],{"class":1047,"line":1662},[826,14173,14101],{"class":1732},[826,14175,2182],{"class":1178},[826,14177,14106],{"class":1182},[826,14179,14180,14182],{"class":1047,"line":1672},[826,14181,14111],{"class":1732},[826,14183,1600],{"class":1178},[826,14185,14186,14188],{"class":1047,"line":1677},[826,14187,14118],{"class":1178},[826,14189,14190],{"class":1182}," flutter build macos --release\n",[826,14192,14193,14195],{"class":1047,"line":1686},[826,14194,14126],{"class":1732},[826,14196,1600],{"class":1178},[826,14198,14199,14201],{"class":1047,"line":1710},[826,14200,14118],{"class":1178},[826,14202,14203],{"class":1182}," build/macos/**/*.app\n",[746,14205,14206],{},"You can trigger both workflows in parallel from your CI, or chain them sequentially. The key insight: don't fight the UI limitations - embrace YAML-based configuration for full control.",[8270,14208],{},[753,14210,14212],{"id":14211},"_2-docker-buildkit-cache-mounts-can-cut-build-times-by-80","2. Docker BuildKit Cache Mounts Can Cut Build Times by 80%",[746,14214,14215,14217],{},[970,14216,14038],{}," Every Docker build reinstalls all dependencies from scratch, even if nothing changed.",[746,14219,14220,14222],{},[970,14221,14044],{}," BuildKit cache mounts persist pip and poetry caches across builds:",[1037,14224,14228],{"className":14225,"code":14226,"language":14227,"meta":865,"style":865},"language-dockerfile shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","RUN --mount=type=cache,mode=0755,target=/root/.cache/pip pip install poetry==1.8.4\nRUN --mount=type=cache,mode=0755,target=/root/.cache/pypoetry poetry install --no-root\n","dockerfile",[1043,14229,14230,14238],{"__ignoreMap":865},[826,14231,14232,14235],{"class":1047,"line":1048},[826,14233,14234],{"class":1573},"RUN",[826,14236,14237],{"class":1051}," --mount=type=cache,mode=0755,target=/root/.cache/pip pip install poetry==1.8.4\n",[826,14239,14240,14242],{"class":1047,"line":866},[826,14241,14234],{"class":1573},[826,14243,14244],{"class":1051}," --mount=type=cache,mode=0755,target=/root/.cache/pypoetry poetry install --no-root\n",[746,14246,14247],{},"This reuses downloaded packages across builds. A typical Python project with 100+ dependencies goes from 3 minutes to 30 seconds on subsequent builds.",[8270,14249],{},[753,14251,14253],{"id":14252},"_3-one-docker-container-two-behaviors","3. One Docker Container, Two Behaviors",[746,14255,14256,14258],{},[970,14257,14038],{}," You need different startup commands for development (hot reload) vs production (gunicorn), but don't want to maintain separate Dockerfiles.",[746,14260,14261,14263],{},[970,14262,14044],{}," You can switch behavior at runtime using environment variables in your command:",[1037,14265,14267],{"className":14052,"code":14266,"language":14054,"meta":865,"style":865},"command: >\n  sh -c \"if [ \\\"$DEVELOPMENT_MODE\\\" = \\\"True\\\" ]; then\n    poetry run python manage.py runserver 0.0.0.0:8000;\n  else\n    gunicorn --bind :8000 --workers 8 musicdatalab.wsgi:application;\n  fi\"\n",[1043,14268,14269,14279,14284,14289,14294,14299],{"__ignoreMap":865},[826,14270,14271,14274,14276],{"class":1047,"line":1048},[826,14272,14273],{"class":1732},"command",[826,14275,2182],{"class":1178},[826,14277,14278],{"class":1499}," >\n",[826,14280,14281],{"class":1047,"line":866},[826,14282,14283],{"class":1182},"  sh -c \"if [ \\\"$DEVELOPMENT_MODE\\\" = \\\"True\\\" ]; then\n",[826,14285,14286],{"class":1047,"line":1060},[826,14287,14288],{"class":1182},"    poetry run python manage.py runserver 0.0.0.0:8000;\n",[826,14290,14291],{"class":1047,"line":1066},[826,14292,14293],{"class":1182},"  else\n",[826,14295,14296],{"class":1047,"line":1072},[826,14297,14298],{"class":1182},"    gunicorn --bind :8000 --workers 8 musicdatalab.wsgi:application;\n",[826,14300,14301],{"class":1047,"line":1218},[826,14302,14303],{"class":1182},"  fi\"\n",[746,14305,14306],{},"Same image, different modes. Deploy once, configure per environment.",[8270,14308],{},[753,14310,14312],{"id":14311},"_4-health-checks-prevent-race-conditions-in-docker-compose","4. Health Checks Prevent Race Conditions in Docker Compose",[746,14314,14315,14317,14318,1282],{},[970,14316,14038],{}," Your app crashes on startup because the database isn't ready yet, even though you used ",[1043,14319,14320],{},"depends_on",[746,14322,14323,14325,14326,14328,14329,14332,14333,14336],{},[970,14324,14044],{}," ",[1043,14327,14320],{}," only waits for the container to ",[763,14330,14331],{},"start",", not for the service to be ",[763,14334,14335],{},"ready",". Use health checks:",[1037,14338,14340],{"className":14052,"code":14339,"language":14054,"meta":865,"style":865},"depends_on:\n  db:\n    condition: service_healthy\n  redis:\n    condition: service_started\n\n# In the db service:\nhealthcheck:\n  test: [\"CMD-SHELL\", \"pg_isready -U myuser\"]\n  interval: 2s\n  timeout: 2s\n  retries: 10\n",[1043,14341,14342,14348,14355,14365,14372,14381,14385,14390,14397,14424,14434,14443],{"__ignoreMap":865},[826,14343,14344,14346],{"class":1047,"line":1048},[826,14345,14320],{"class":1732},[826,14347,1600],{"class":1178},[826,14349,14350,14353],{"class":1047,"line":866},[826,14351,14352],{"class":1732},"  db",[826,14354,1600],{"class":1178},[826,14356,14357,14360,14362],{"class":1047,"line":1060},[826,14358,14359],{"class":1732},"    condition",[826,14361,2182],{"class":1178},[826,14363,14364],{"class":1182}," service_healthy\n",[826,14366,14367,14370],{"class":1047,"line":1066},[826,14368,14369],{"class":1732},"  redis",[826,14371,1600],{"class":1178},[826,14373,14374,14376,14378],{"class":1047,"line":1072},[826,14375,14359],{"class":1732},[826,14377,2182],{"class":1178},[826,14379,14380],{"class":1182}," service_started\n",[826,14382,14383],{"class":1047,"line":1218},[826,14384,1547],{"emptyLinePlaceholder":877},[826,14386,14387],{"class":1047,"line":1229},[826,14388,14389],{"class":1169},"# In the db service:\n",[826,14391,14392,14395],{"class":1047,"line":1584},[826,14393,14394],{"class":1732},"healthcheck",[826,14396,1600],{"class":1178},[826,14398,14399,14402,14404,14406,14408,14411,14413,14415,14417,14420,14422],{"class":1047,"line":1589},[826,14400,14401],{"class":1732},"  test",[826,14403,2182],{"class":1178},[826,14405,6350],{"class":1178},[826,14407,1292],{"class":1178},[826,14409,14410],{"class":1182},"CMD-SHELL",[826,14412,1292],{"class":1178},[826,14414,1299],{"class":1178},[826,14416,1557],{"class":1178},[826,14418,14419],{"class":1182},"pg_isready -U myuser",[826,14421,1292],{"class":1178},[826,14423,2390],{"class":1178},[826,14425,14426,14429,14431],{"class":1047,"line":1603},[826,14427,14428],{"class":1732},"  interval",[826,14430,2182],{"class":1178},[826,14432,14433],{"class":1182}," 2s\n",[826,14435,14436,14439,14441],{"class":1047,"line":1615},[826,14437,14438],{"class":1732},"  timeout",[826,14440,2182],{"class":1178},[826,14442,14433],{"class":1182},[826,14444,14445,14448,14450],{"class":1047,"line":1620},[826,14446,14447],{"class":1732},"  retries",[826,14449,2182],{"class":1178},[826,14451,14452],{"class":1573}," 10\n",[746,14454,14455],{},"Now Docker waits until PostgreSQL actually accepts connections.",[8270,14457],{},[753,14459,14461],{"id":14460},"_5-celery-workers-can-auto-restart-on-memory-leaks","5. Celery Workers Can Auto-Restart on Memory Leaks",[746,14463,14464,14466],{},[970,14465,14038],{}," Your Celery workers slowly consume more memory until they get OOM-killed, taking running tasks with them.",[746,14468,14469,14471],{},[970,14470,14044],{}," Celery has a built-in flag to gracefully restart workers that exceed a memory threshold:",[1037,14473,14475],{"className":14052,"code":14474,"language":14054,"meta":865,"style":865},"command: celery -A myapp worker --loglevel=info --concurrency=4 --max-memory-per-child=100000\n",[1043,14476,14477],{"__ignoreMap":865},[826,14478,14479,14481,14483],{"class":1047,"line":1048},[826,14480,14273],{"class":1732},[826,14482,2182],{"class":1178},[826,14484,14485],{"class":1182}," celery -A myapp worker --loglevel=info --concurrency=4 --max-memory-per-child=100000\n",[746,14487,14488],{},"Workers restart themselves after processing tasks if they exceed ~100MB. Memory leaks become a minor nuisance instead of a production incident.",[8270,14490],{},[753,14492,14494],{"id":14493},"_6-firebases-immutable-cache-headers-give-you-free-cdn-performance","6. Firebase's Immutable Cache Headers Give You Free CDN Performance",[746,14496,14497,14499],{},[970,14498,14038],{}," Your static assets (images, fonts, JS bundles) get re-downloaded on every visit, wasting bandwidth and slowing load times.",[746,14501,14502,14504,14505,14508],{},[970,14503,14044],{}," If your assets have hashed filenames (like ",[1043,14506,14507],{},"app.a1b2c3.js","), you can cache them forever:",[1037,14510,14512],{"className":8472,"code":14511,"language":2359,"meta":865,"style":865},"{\n  \"headers\": [\n    {\n      \"source\": \"**/*.@(jpg|jpeg|gif|png|svg|webp|ico|avif)\",\n      \"headers\": [{\n        \"key\": \"Cache-Control\",\n        \"value\": \"public, max-age=31536000, immutable\"\n      }]\n    }\n  ]\n}\n",[1043,14513,14514,14518,14530,14535,14556,14568,14588,14606,14611,14615,14619],{"__ignoreMap":865},[826,14515,14516],{"class":1047,"line":1048},[826,14517,8480],{"class":1178},[826,14519,14520,14522,14524,14526,14528],{"class":1047,"line":866},[826,14521,8485],{"class":1178},[826,14523,6441],{"class":1592},[826,14525,1292],{"class":1178},[826,14527,2182],{"class":1178},[826,14529,6994],{"class":1178},[826,14531,14532],{"class":1047,"line":1060},[826,14533,14534],{"class":1178},"    {\n",[826,14536,14537,14540,14543,14545,14547,14549,14552,14554],{"class":1047,"line":1066},[826,14538,14539],{"class":1178},"      \"",[826,14541,14542],{"class":1596},"source",[826,14544,1292],{"class":1178},[826,14546,2182],{"class":1178},[826,14548,1557],{"class":1178},[826,14550,14551],{"class":1182},"**/*.@(jpg|jpeg|gif|png|svg|webp|ico|avif)",[826,14553,1292],{"class":1178},[826,14555,2159],{"class":1178},[826,14557,14558,14560,14562,14564,14566],{"class":1047,"line":1072},[826,14559,14539],{"class":1178},[826,14561,6441],{"class":1596},[826,14563,1292],{"class":1178},[826,14565,2182],{"class":1178},[826,14567,5720],{"class":1178},[826,14569,14570,14572,14575,14577,14579,14581,14584,14586],{"class":1047,"line":1218},[826,14571,3820],{"class":1178},[826,14573,14574],{"class":1573},"key",[826,14576,1292],{"class":1178},[826,14578,2182],{"class":1178},[826,14580,1557],{"class":1178},[826,14582,14583],{"class":1182},"Cache-Control",[826,14585,1292],{"class":1178},[826,14587,2159],{"class":1178},[826,14589,14590,14592,14595,14597,14599,14601,14604],{"class":1047,"line":1229},[826,14591,3820],{"class":1178},[826,14593,14594],{"class":1573},"value",[826,14596,1292],{"class":1178},[826,14598,2182],{"class":1178},[826,14600,1557],{"class":1178},[826,14602,14603],{"class":1182},"public, max-age=31536000, immutable",[826,14605,1563],{"class":1178},[826,14607,14608],{"class":1047,"line":1584},[826,14609,14610],{"class":1178},"      }]\n",[826,14612,14613],{"class":1047,"line":1589},[826,14614,3918],{"class":1178},[826,14616,14617],{"class":1047,"line":1603},[826,14618,11106],{"class":1178},[826,14620,14621],{"class":1047,"line":1615},[826,14622,3495],{"class":1178},[746,14624,14625],{},"One year cache, marked immutable. Browsers won't even send conditional requests. Combined with Nuxt or Next.js hash-based filenames, cache invalidation happens automatically on deploy.",[8270,14627],{},[753,14629,14631],{"id":14630},"_7-pre-commit-hooks-can-enforce-conventional-commits","7. Pre-commit Hooks Can Enforce Conventional Commits",[746,14633,14634,14636],{},[970,14635,14038],{}," Your git history is a mess of \"fix\", \"update\", and \"wip\" messages. You want semantic versioning but can't parse the commits.",[746,14638,14639,14641],{},[970,14640,14044],{}," You can reject non-conventional commits before they're even created:",[1037,14643,14645],{"className":14052,"code":14644,"language":14054,"meta":865,"style":865},"# .pre-commit-config.yaml\n- repo: https://github.com/compilerla/conventional-pre-commit\n  rev: v3.0.0\n  hooks:\n    - id: conventional-pre-commit\n      stages: [commit-msg]\n",[1043,14646,14647,14652,14664,14674,14681,14694],{"__ignoreMap":865},[826,14648,14649],{"class":1047,"line":1048},[826,14650,14651],{"class":1169},"# .pre-commit-config.yaml\n",[826,14653,14654,14656,14659,14661],{"class":1047,"line":866},[826,14655,2439],{"class":1178},[826,14657,14658],{"class":1732}," repo",[826,14660,2182],{"class":1178},[826,14662,14663],{"class":1182}," https://github.com/compilerla/conventional-pre-commit\n",[826,14665,14666,14669,14671],{"class":1047,"line":1060},[826,14667,14668],{"class":1732},"  rev",[826,14670,2182],{"class":1178},[826,14672,14673],{"class":1182}," v3.0.0\n",[826,14675,14676,14679],{"class":1047,"line":1066},[826,14677,14678],{"class":1732},"  hooks",[826,14680,1600],{"class":1178},[826,14682,14683,14686,14689,14691],{"class":1047,"line":1072},[826,14684,14685],{"class":1178},"    -",[826,14687,14688],{"class":1732}," id",[826,14690,2182],{"class":1178},[826,14692,14693],{"class":1182}," conventional-pre-commit\n",[826,14695,14696,14699,14701,14703,14706],{"class":1047,"line":1218},[826,14697,14698],{"class":1732},"      stages",[826,14700,2182],{"class":1178},[826,14702,6350],{"class":1178},[826,14704,14705],{"class":1182},"commit-msg",[826,14707,2390],{"class":1178},[746,14709,14710,14711,14714,14715,14718],{},"Now ",[1043,14712,14713],{},"git commit -m \"fixed stuff\""," fails, but ",[1043,14716,14717],{},"git commit -m \"fix: resolve login timeout issue\""," succeeds. Your changelog writes itself.",[8270,14720],{},[753,14722,14724],{"id":14723},"_8-shell-scripts-can-fail-fast-with-dependency-checks","8. Shell Scripts Can Fail Fast with Dependency Checks",[746,14726,14727,14729],{},[970,14728,14038],{}," Your startup script fails midway through because a required Python module is missing, leaving the system in a partial state.",[746,14731,14732,14734],{},[970,14733,14044],{}," You can validate critical imports before starting:",[1037,14736,14738],{"className":1160,"code":14737,"language":1162,"meta":865,"style":865},"#!/usr/bin/env sh\nset -eu\n\n# Fail-fast import check\npython - \u003C\u003C'PY'\nimport importlib\nfor m in (\"fastapi\", \"uvicorn\", \"sqlalchemy\"):\n    importlib.import_module(m)\nPY\n\nexec python -m uvicorn app.main:app --host 0.0.0.0 --port \"$PORT\"\n",[1043,14739,14740,14745,14752,14756,14761,14774,14779,14784,14789,14794,14798],{"__ignoreMap":865},[826,14741,14742],{"class":1047,"line":1048},[826,14743,14744],{"class":1169},"#!/usr/bin/env sh\n",[826,14746,14747,14749],{"class":1047,"line":866},[826,14748,2460],{"class":1285},[826,14750,14751],{"class":1182}," -eu\n",[826,14753,14754],{"class":1047,"line":1060},[826,14755,1547],{"emptyLinePlaceholder":877},[826,14757,14758],{"class":1047,"line":1066},[826,14759,14760],{"class":1169},"# Fail-fast import check\n",[826,14762,14763,14765,14768,14771],{"class":1047,"line":1072},[826,14764,1262],{"class":1596},[826,14766,14767],{"class":1182}," -",[826,14769,14770],{"class":1178}," \u003C\u003C",[826,14772,14773],{"class":1178},"'PY'\n",[826,14775,14776],{"class":1047,"line":1218},[826,14777,14778],{"class":1182},"import importlib\n",[826,14780,14781],{"class":1047,"line":1229},[826,14782,14783],{"class":1182},"for m in (\"fastapi\", \"uvicorn\", \"sqlalchemy\"):\n",[826,14785,14786],{"class":1047,"line":1584},[826,14787,14788],{"class":1182},"    importlib.import_module(m)\n",[826,14790,14791],{"class":1047,"line":1589},[826,14792,14793],{"class":1178},"PY\n",[826,14795,14796],{"class":1047,"line":1603},[826,14797,1547],{"emptyLinePlaceholder":877},[826,14799,14800,14803,14806,14809,14812,14815,14818,14821,14824,14826,14829],{"class":1047,"line":1615},[826,14801,14802],{"class":1285},"exec",[826,14804,14805],{"class":1182}," python",[826,14807,14808],{"class":1182}," -m",[826,14810,14811],{"class":1182}," uvicorn",[826,14813,14814],{"class":1182}," app.main:app",[826,14816,14817],{"class":1182}," --host",[826,14819,14820],{"class":1573}," 0.0.0.0",[826,14822,14823],{"class":1182}," --port",[826,14825,1557],{"class":1178},[826,14827,14828],{"class":1051},"$PORT",[826,14830,1563],{"class":1178},[746,14832,14833],{},"If any import fails, the script exits immediately with a clear error. No partial startups.",[8270,14835],{},[753,14837,14839],{"id":14838},"_9-nuxt-can-auto-discover-routes-by-crawling-links","9. Nuxt Can Auto-Discover Routes by Crawling Links",[746,14841,14842,14844],{},[970,14843,14038],{}," You're using Nuxt's static generation but have to manually list every route for prerendering.",[746,14846,14847,14849],{},[970,14848,14044],{}," Nuxt can automatically discover pages by following internal links:",[1037,14851,14855],{"className":14852,"code":14853,"language":14854,"meta":865,"style":865},"language-typescript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","// nuxt.config.ts\nexport default defineNuxtConfig({\n  nitro: {\n    preset: 'static',\n    prerender: {\n      routes: ['/'],\n      crawlLinks: true  // Magic happens here\n    }\n  }\n})\n","typescript",[1043,14856,14857,14862,14875,14884,14900,14909,14928,14942,14946,14950],{"__ignoreMap":865},[826,14858,14859],{"class":1047,"line":1048},[826,14860,14861],{"class":1169},"// nuxt.config.ts\n",[826,14863,14864,14866,14868,14871,14873],{"class":1047,"line":866},[826,14865,9612],{"class":1499},[826,14867,1303],{"class":1499},[826,14869,14870],{"class":1285}," defineNuxtConfig",[826,14872,1289],{"class":1051},[826,14874,8480],{"class":1178},[826,14876,14877,14880,14882],{"class":1047,"line":1060},[826,14878,14879],{"class":1732},"  nitro",[826,14881,2182],{"class":1178},[826,14883,3815],{"class":1178},[826,14885,14886,14889,14891,14893,14896,14898],{"class":1047,"line":1066},[826,14887,14888],{"class":1732},"    preset",[826,14890,2182],{"class":1178},[826,14892,9703],{"class":1178},[826,14894,14895],{"class":1182},"static",[826,14897,7358],{"class":1178},[826,14899,2159],{"class":1178},[826,14901,14902,14905,14907],{"class":1047,"line":1072},[826,14903,14904],{"class":1732},"    prerender",[826,14906,2182],{"class":1178},[826,14908,3815],{"class":1178},[826,14910,14911,14914,14916,14918,14920,14922,14924,14926],{"class":1047,"line":1218},[826,14912,14913],{"class":1732},"      routes",[826,14915,2182],{"class":1178},[826,14917,6350],{"class":1051},[826,14919,7358],{"class":1178},[826,14921,5334],{"class":1182},[826,14923,7358],{"class":1178},[826,14925,3521],{"class":1051},[826,14927,2159],{"class":1178},[826,14929,14930,14933,14935,14939],{"class":1047,"line":1229},[826,14931,14932],{"class":1732},"      crawlLinks",[826,14934,2182],{"class":1178},[826,14936,14938],{"class":14937},"sfNiH"," true",[826,14940,14941],{"class":1169},"  // Magic happens here\n",[826,14943,14944],{"class":1047,"line":1584},[826,14945,3918],{"class":1178},[826,14947,14948],{"class":1047,"line":1589},[826,14949,9416],{"class":1178},[826,14951,14952,14954],{"class":1047,"line":1603},[826,14953,2153],{"class":1178},[826,14955,1314],{"class":1051},[746,14957,14958,14959,14961],{},"Start from ",[1043,14960,5334],{},", and Nuxt will find and prerender every linked page. Add new content, link to it from anywhere, and it's automatically included in the build.",[8270,14963],{},[753,14965,14967],{"id":14966},"_10-aws-in-docker-disable-ec2-metadata-to-force-environment-variables","10. AWS in Docker: Disable EC2 Metadata to Force Environment Variables",[746,14969,14970,14972],{},[970,14971,14038],{}," Your containerized app ignores the AWS credentials you set in environment variables and tries to use EC2 metadata (which doesn't exist outside EC2).",[746,14974,14975,14977],{},[970,14976,14044],{}," You can explicitly disable the metadata service:",[1037,14979,14981],{"className":14052,"code":14980,"language":14054,"meta":865,"style":865},"environment:\n  - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}\n  - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}\n  - AWS_EC2_METADATA_DISABLED=true\n  - AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=\n",[1043,14982,14983,14990,14998,15005,15012],{"__ignoreMap":865},[826,14984,14985,14988],{"class":1047,"line":1048},[826,14986,14987],{"class":1732},"environment",[826,14989,1600],{"class":1178},[826,14991,14992,14995],{"class":1047,"line":866},[826,14993,14994],{"class":1178},"  -",[826,14996,14997],{"class":1182}," AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}\n",[826,14999,15000,15002],{"class":1047,"line":1060},[826,15001,14994],{"class":1178},[826,15003,15004],{"class":1182}," AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}\n",[826,15006,15007,15009],{"class":1047,"line":1066},[826,15008,14994],{"class":1178},[826,15010,15011],{"class":1182}," AWS_EC2_METADATA_DISABLED=true\n",[826,15013,15014,15016],{"class":1047,"line":1072},[826,15015,14994],{"class":1178},[826,15017,15018],{"class":1182}," AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=\n",[746,15020,15021],{},"The AWS SDK has a credential chain that checks metadata before environment variables. Disabling metadata forces it to use your explicit credentials.",[8270,15023],{},[753,15025,15027],{"id":15026},"bonus-ruff-lets-you-apply-different-rules-to-different-paths","Bonus: Ruff Lets You Apply Different Rules to Different Paths",[746,15029,15030,15032],{},[970,15031,14044],{}," You can be strict in production code but lenient in tests:",[1037,15034,15038],{"className":15035,"code":15036,"language":15037,"meta":865,"style":865},"language-toml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# ruff.toml\n[lint.per-file-ignores]\n\"__init__.py\" = [\"E402\"]  # Allow late imports in init files\n\"**/tests/*\" = [\"E402\", \"S101\"]  # Allow assert statements in tests\n\"**/migrations/*\" = [\"E501\"]  # Allow long lines in migrations\n","toml",[1043,15039,15040,15045,15059,15084,15117],{"__ignoreMap":865},[826,15041,15042],{"class":1047,"line":1048},[826,15043,15044],{"class":1169},"# ruff.toml\n",[826,15046,15047,15049,15052,15054,15057],{"class":1047,"line":866},[826,15048,2380],{"class":1178},[826,15050,15051],{"class":1596},"lint",[826,15053,1282],{"class":1051},[826,15055,15056],{"class":1596},"per-file-ignores",[826,15058,2390],{"class":1178},[826,15060,15061,15063,15066,15068,15070,15072,15074,15077,15079,15081],{"class":1047,"line":1060},[826,15062,1292],{"class":1178},[826,15064,15065],{"class":1051},"__init__.py",[826,15067,1292],{"class":1178},[826,15069,1763],{"class":1178},[826,15071,6350],{"class":1178},[826,15073,1292],{"class":1178},[826,15075,15076],{"class":1182},"E402",[826,15078,1292],{"class":1178},[826,15080,3521],{"class":1178},[826,15082,15083],{"class":1169},"  # Allow late imports in init files\n",[826,15085,15086,15088,15091,15093,15095,15097,15099,15101,15103,15105,15107,15110,15112,15114],{"class":1047,"line":1066},[826,15087,1292],{"class":1178},[826,15089,15090],{"class":1051},"**/tests/*",[826,15092,1292],{"class":1178},[826,15094,1763],{"class":1178},[826,15096,6350],{"class":1178},[826,15098,1292],{"class":1178},[826,15100,15076],{"class":1182},[826,15102,1292],{"class":1178},[826,15104,1299],{"class":1178},[826,15106,1557],{"class":1178},[826,15108,15109],{"class":1182},"S101",[826,15111,1292],{"class":1178},[826,15113,3521],{"class":1178},[826,15115,15116],{"class":1169},"  # Allow assert statements in tests\n",[826,15118,15119,15121,15124,15126,15128,15130,15132,15135,15137,15139],{"class":1047,"line":1072},[826,15120,1292],{"class":1178},[826,15122,15123],{"class":1051},"**/migrations/*",[826,15125,1292],{"class":1178},[826,15127,1763],{"class":1178},[826,15129,6350],{"class":1178},[826,15131,1292],{"class":1178},[826,15133,15134],{"class":1182},"E501",[826,15136,1292],{"class":1178},[826,15138,3521],{"class":1178},[826,15140,15141],{"class":1169},"  # Allow long lines in migrations\n",[746,15143,15144],{},"No more disabling rules globally just because they don't apply everywhere.",[8270,15146],{},[753,15148,10705],{"id":10704},[746,15150,15151],{},"This is Part 1 of an ongoing series. Each installment will bring 10 new tips from real codebases - no fluff, just practical knowledge.",[746,15153,15154,15155,15160],{},"Got a tip of your own? Found something clever in a codebase you're working on? ",[922,15156,15159],{"href":15157,"rel":15158},"https://musictechlab.io/contact",[926],"Let us know"," - we might feature it in a future edition.",[8170,15162,15163],{},"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 .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 .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}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 .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":865,"searchDepth":866,"depth":866,"links":15165},[15166,15167,15168,15169,15170,15171,15172,15173,15174,15175,15176,15177],{"id":14032,"depth":866,"text":14033},{"id":14211,"depth":866,"text":14212},{"id":14252,"depth":866,"text":14253},{"id":14311,"depth":866,"text":14312},{"id":14460,"depth":866,"text":14461},{"id":14493,"depth":866,"text":14494},{"id":14630,"depth":866,"text":14631},{"id":14723,"depth":866,"text":14724},{"id":14838,"depth":866,"text":14839},{"id":14966,"depth":866,"text":14967},{"id":15026,"depth":866,"text":15027},{"id":10704,"depth":866,"text":10705},"2026-01-13T00:00:00.000Z","A collection of practical software development tips, workarounds, and clever solutions discovered in production codebases. Part 1 of an ongoing series.",{"src":15181},"/images/blog/musictechlab_blog_did-you-know-dev-tips.webp",{"enabled":877,"items":15183},[15184,15186,15189],{"text":15185,"icon":8208},"Docker BuildKit cache mounts can cut Python build times from 3 minutes to 30 seconds.",{"text":15187,"icon":15188},"Codemagic requires separate YAML workflows for iOS and macOS builds.","i-lucide-smartphone",{"text":15190,"icon":890},"One Docker container can serve dev and production with an environment variable switch.",{},{"title":454,"description":15179},[894],"Kb9D4yTYHbtDY87-WwjGBOBbdzA3zOs13LjDaWwrYNE",1780305300651]