הטמעת מיני־אפליקציה
הריצו במוצר שלכם מיני־אפליקציה של Nodaro שפורסמה, עם קוד ההטמעה או עם טופס משלכם על ה-REST API: מקריאת הקלטים ועד הרצות, תוצאות ושגיאות.
הטמעת מיני־אפליקציה פירושה הרצה של אפליקציית Nodaro שכבר פורסמה, מתוך המוצר שלכם. אפשר להדביק בדף את קוד ההטמעה של האפליקציה, או לבנות טופס משלכם על ה-REST API: לקרוא את הקלטים של האפליקציה, להתחיל הרצה, לבדוק את הסטטוס שלה מדי כמה שניות ולהציג את התוצאה. האפליקציה עצמה נשארת ב-Nodaro; המוצר שלכם אוסף את הקלטים ומציג את הפלטים.
שתי דרכים להטמיע
| קוד הטמעה | ממשק משלכם | |
|---|---|---|
| מאמץ | הדבקה של תג iframe אחד. | בניית טופס ונתיב קטן בשרת. |
| מראה | התצוגה של האפליקציה ב-Nodaro. | העיצוב שלכם. |
| בשם מי ההרצה פועלת | הצופה, עם הסשן שלו ב-Nodaro. | החשבון של האסימון שלכם. |
הדבקת קוד ההטמעה
פתיחת הגדרות ההטמעה של האפליקציה
ב-Nodaro, פתחו את הדף מיני־אפליקציות, בחרו בלשונית המיני־אפליקציות שלי ולחצו על הטמעה בכרטיס של האפליקציה.
אישור הדומיין שלכם
תחת דומיינים מורשים להטמעה, הקלידו את ה-origin של האתר שלכם, כגון https://example.com, ולחצו על הוספה. ההטמעה חסומה עד שמוסיפים לפחות דומיין אחד.
העתקת הקוד לדף שלכם
לחצו על העתקת קוד הטמעה והדביקו את התג בדף שלכם:
<iframe src="https://app.nodaro.ai/embed/your-app-slug" width="100%" height="600" frameborder="0" allow="clipboard-write"></iframe>בניית ממשק משלכם
שאר הדף מסביר איך לבנות על ה-REST API ממשק ווב או מובייל משלכם לאפליקציה שפורסמה. ההסבר שלם בפני עצמו, ולכן אפשר גם לתת אותו למחולל קוד AI: הוסיפו .md לסוף הכתובת של הדף הזה כדי לקבל עותק ב-Markdown.
שלושה דברים שכדאי לדעת קודם
| כתובת הבסיס | https://app.nodaro.ai ב-Nodaro Cloud, או הכתובת של ההתקנה שלכם. |
| מזהה האפליקציה | ה-slug, כלומר החלק האחרון בכתובת ה-URL של האפליקציה שפורסמה. בכתובת https://app.nodaro.ai/app/my-cool-app, ה-slug הוא my-cool-app. |
| אופן ההרצה | אסינכרוני. הנתיב POST /v1/app/{slug}/run מחזיר מיד runId, ואתם בודקים את הסטטוס מדי כמה שניות עם GET /v1/app/{slug}/runs/{runId} עד שהתוצאה מוכנה. ההרצה לא שולחת אליכם קריאה חוזרת. |
שתי נקודות קצה ציבוריות, שלא דורשות אסימון, נותנות לכם את כל מה שצריך לטופס:
- הנתיב
GET /v1/app/{slug}מחזיר את הקלטים ואת המטא־נתונים של האפליקציה. - הנתיב
GET /v1/nodes/{type}מחזיר את השדות של סוג רכיב אחד.
שלב 1: קריאת הקלטים של האפליקציה
curl https://app.nodaro.ai/v1/app/<slug>השדות שחשובים לממשק שלכם:
{
"id": "uuid",
"name": "Headline Generator",
"description": "...",
"iconUrl": "https://...",
"version": 3, // the latest version
"estimatedCredits": 5, // the listed price: a run, with any Render final after it
"perMinuteCredits": 0, // per minute of the recording the app is given; 0 when the price does not depend on it
"perItemCredits": 0, // per item beyond the saved items of a list the user fills; 0 when none
"maxEstimatedCredits": 9, // "up to": the most a run can cost over the user's choices (with
"maxPerMinuteCredits": 0, // maxPerMinuteCredits and maxPerItemCredits); present only when it
"maxPerItemCredits": 0, // differs from the "from" figures above
"estimatedCreditsIsEstimate": true, // present when the price holds a metered LLM call's estimate: show "~5"
"runEstimatedCredits": 5, // the run alone: the listed price without its Render final part
"runPerMinuteCredits": 0, // ...and its per-minute and per-item parts
"runPerItemCredits": 0,
"editPlanPerMinute": false, // true when an Edit Plan node is charged per started minute of the recording
"maxRunsPerUserPerDay": null, // or a number
"thumbnailNodeId": "node-abc", // the node whose output is the main result
"snapshotNodes": [ // the workflow's nodes
{
"id": "node-abc",
"type": "generate-image", // use it in step 2
"data": {
"prompt": "default prompt", // the current value of each field
"aspectRatio": "1:1"
}
}
],
"snapshotEdges": [ /* the connections, rarely needed by your interface */ ],
"snapshotSettings": {
"presentationSettings": {
"inputItems": [ // the form schema
{ "type": "field", "id": "item-1", "nodeId": "node-abc", "field": "prompt", "allowedValues": null, "maxLength": 1200 },
{ "type": "field", "id": "item-2", "nodeId": "node-abc", "field": "aspectRatio", "allowedValues": ["1:1", "16:9", "9:16"] }
]
}
},
"versions": [{ "version": 3, "id": "...", "createdAt": "..." }]
}השדה inputItems הוא הטופס שהמפרסם עיצב. עברו עליו, היכנסו לכל group ואספו את כל הפריטים מסוג field: זה הטופס שלכם.
type | איך להציג אותו |
|---|---|
field | שדה בטופס. השתמשו ב-nodeId, ב-field, ב-allowedValues האופציונלי וב-maxLength האופציונלי, מגבלת התווים של שדה טקסט. |
node | תצוגת ברירת המחדל של הרכיב כולו. בטופס משלכם, דלגו על הפריט הזה או הציגו כל שדה של הרכיב. גם רכיב טקסט שנחשף כולו יכול לכלול maxLength. |
output | תצוגה מקדימה חיה של תוצאה. התעלמו מהפריט הזה כשאתם בונים את הטופס, והציגו אותו אחרי ההרצה. |
richtext | טקסט Markdown קבוע שהמפרסם כתב. הציגו אותו כמו שהוא. |
group | קבוצה עם items משלה. היכנסו אליה; אין קבוצות בתוך קבוצות. |
שלב 2: בירור הסוג של כל שדה
השדה inputItems נותן לכם זוגות של nodeId ו-field, אבל לא את סוג השדה: טקסט, מספר או בחירה. כדי לדעת יותר, בדקו את התיאור של סוג הרכיב:
curl https://app.nodaro.ai/v1/nodes/generate-image{
"data": {
"type": "generate-image",
"label": "Generate Image",
"category": "ai-image",
"description": "...",
"outputType": "image",
"providers": ["nano-banana-pro", "gpt-image-2", "flux"], // shortened
"capabilities": ["supports-reference-image", "supports-negative-prompt"]
}
}לא לכל מתאר רכיב יש inputSchema מלא. כשהוא חסר, הסיקו מהו הפקד לפי הכללים האלה, בסדר הזה:
- לפריט הקלט יש
allowedValues: רשימה נפתחת עם האפשרויות האלה. - הערך הנוכחי ב-
snapshotNodes[i].data[field]הוא בוליאני: מתג. - הערך הנוכחי הוא מספר: שדה מספרי, או מחוון כששם השדה מסתיים ב-
duration, ב-intensity, ב-strength, ב-scaleאו ב-temperature. - הערך הנוכחי הוא מחרוזת, ושם השדה הוא
prompt,description,text,content,captionאוmessage: שדה טקסט מרובה שורות. - הערך הנוכחי הוא כתובת URL, וסוג הרכיב מתחיל ב-
upload-: העלאת קובץ ששולחת את כתובת ה-URL הציבורית של הקובץ. - כל מקרה אחר: שדה טקסט של שורה אחת.
שמות השדות האלה מכסים את רוב השדות שאפליקציות חושפות:
| שם השדה | פקד | הערות |
|---|---|---|
prompt, negativePrompt, text, description | שדה טקסט מרובה שורות | |
model, provider, voice, style, tone | רשימה נפתחת | האפשרויות מגיעות מ-allowedValues או מ-providers. |
aspectRatio | רשימה נפתחת | בדרך כלל 1:1, 16:9, 9:16, 4:3 ו-3:4. |
resolution | רשימה נפתחת | בדרך כלל 1K, 2K ו-4K, או 720p, 1080p ו-4k. |
quality | רשימה נפתחת | בדרך כלל medium ו-high. |
duration, nFrames, seed, temperature | מספר | |
enableTranslation, addAudio, headless, loop | מתג | |
imageUrl, videoUrl, audioUrl, referenceImage | כתובת URL | העלו קודם את הקובץ, ואז שלחו את הכתובת שלו. |
דרך MCP, הכלי get_app_inputs מציין בשבילכם את הסוג של חלק מהשדות. מחוון שהאפליקציה חושפת חוזר כ-type: "number", עם ה-min, ה-max וה-step שלו. שדה שהמפרסם הגביל לרשימת ערכים חוזר כ-type: "select", עם ה-options האלה. שדה מההגדרות המהירות של הרכיב חוזר כ-select, כ-number או כ-boolean; ראו את הסעיף שדות של מודל ושל הגדרות מהירות. כל שדה אחר חוזר כ-type: "text", ולכן בדקו אותו בתיאור של הרכיב. קלט שטוח של מחוון מקבל מספר או מחרוזת מספרית.
מחוונים ומתגים. מחוון או מתג מתחילים מהערך השמור של הרכיב, ב-snapshotNodes[i].data[field]. כשאין לרכיב ערך שמור, האפליקציה מציגה את ברירת המחדל של השדה: הערך שרכיב חדש מקבל. הרצה שולחת שדה רק אחרי שהמשתמש שינה אותו. עד אז חל הערך של הרכיב עצמו. עשו את אותו הדבר בממשק שלכם: מלאו מראש את הפקד, ושלחו את השדה רק כשהמשתמש שלכם משנה אותו. הגדרות הקול של רכיב טקסט לדיבור מקבלות מספר או מחרוזת מספרית, וערך מחוץ לטווח של ההגדרה מוצמד לקצה הקרוב של הטווח.
שדות של מודל ושל הגדרות מהירות
אפשר לחשוף באפליקציה כל הגדרה מהסרגל התחתון של רכיב בעורך: את המודל, וברוב רכיבי היצירה גם את הרזולוציה, המשך, יחס הגובה־רוחב, ההגדרה טיוטה והגדרות דומות. למשל, רכיב וידאו לווידאו יכול לחשוף את ההגדרות מודל, רזולוציה וטיוטה, ורכיב יצירת וידאו את ההגדרות משך, רזולוציה, יחס וטיוטה, לצד ההגדרה מודל. ההגדרה הרצות, שקובעת את מספר ההרצות, אף פעם לא מוצעת.
- המפתח הוא שדה הנתונים של הרכיב עצמו, כגון
provider,v2vResolution,durationאוdraft. שלחו את הערך ב-inputOverrides[nodeId][field], בסוג שהרכיב שומר: הערך8בשדה של משך, ולא"8", והערךtrueבשדה של טיוטה. - רשימה קבועה נאכפת. חלק מההגדרות מציעות את אותם ערכים בלי קשר לשאר ההגדרות של הרכיב, כגון המודלים שרכיב מציע, מודל של Suno או יחס הגובה־רוחב של סצנת תלת־ממד. ערך מחוץ לרשימה נדחה עם
400 validation_error, וההודעה היאInvalid value for provider: …. Allowed: …(ערך לא תקין ל-provider: …. ערכים מותרים: …). המפרסם יכול לצמצם את הרשימות האלה עוד יותר עםallowedValues. - טווח נאכף. הגדרה שמקבלת כל מספר בטווח, כגון ההגדרה אורך של סצנת תלת־ממד או ההגדרה סף של רכיב מסכת וידאו, דוחה מספר מחוץ לטווח עם
400 validation_error. - חלק מהאפשרויות נקבעות לפי המודל. המודל של הרכיב קובע אילו רזולוציות, יחסי גובה־רוחב ומשכי זמן הגדרה מציעה. הכרטיס של האפליקציה מציע את האפשרויות של המודל שההרצה תשתמש בו, וכשהמודל מתחלף, אפשרות שהמודל החדש לא תומך בה מתחלפת באפשרות שהוא תומך בה. עשו את אותו הדבר בטופס שלכם: כלי ה-MCP
get_app_inputsמפרט את הערכים של כל המודלים לשדה כזה, והשדהsnapshotNodes[i].data.provider, או הערך שהטופס שלכם שולח, מציין איזה מודל רץ. - טיוטה מרונדרת ב-480p. במודלים של Seedance, הכרטיס של האפליקציה גם מגדיר את הרזולוציה ל-480p כשההגדרה טיוטה מופעלת, והשרת מרנדר טיוטה ב-480p בלי קשר לרזולוציה שנשלחה.
ניקוי שדה
שדה שמוגדר ל-null ב-inputOverrides מתנקה בהרצה הזו: הרכיב רץ כאילו אין לו ערך בשדה, לא null וגם לא הערך שהיוצר שמר, ולכן חלה ברירת המחדל של הרכיב עצמו. הכרטיס של האפליקציה עושה את זה בהגדרה שאין למודל שנבחר. למשל, כשמחליפים את המודל של רכיב וידאו לווידאו ל-Runway Aleph, שאין לו הגדרת רזולוציה, הרזולוציה מתנקה. הכרטיס גם מכבה כך את ההגדרה טיוטה. שדה שנוקה אף פעם לא נדחה בגלל allowedValues או בגלל רשימה קבועה.
בקלט שטוח דרך MCP או דרך ה-SDK, הערך null מושמט לפני שהוא מגיע לרכיב, ולכן חל הערך השמור של הרכיב. כדי לנקות שדה, השתמשו ב-inputOverrides.
שדות של משבצות Lottie
רכיב גרפיקת תנועה על מנוע Lottie יכול לחשוף כקלטים של האפליקציה את המשבצות שלו שיש להן שם, כגון צבעים, טקסטים ומספרים. הן מופיעות כפריטי שדה שה-field שלהם מתחיל ב-slot:, כגון slot:primaryColor. המשתמשים משנים אותן בכל הרצה תמורת 0 קרדיטים: כשאפליקציה חושפת שדות של משבצות, כל הרצה משתמשת שוב באנימציה שפורסמה ומחליפה רק את ערכי המשבצות.
| ערך המשבצת | פקד | הערך לשליחה |
|---|---|---|
| צבע, כמערך RGBA של מספרים מ-0 עד 1 | פקד בחירת צבע | מחרוזת הקסדצימלית, כגון "#00ff00" |
| מחרוזת | שדה טקסט | המחרוזת |
| מספר | מחוון | המספר |
ערך של משבצת הוא לא שדה רגיל של רכיב: הוא נמצא ב-motionPlan.slotValues של הרכיב. כדי לקבוע משבצות דרך inputOverrides גולמי, החליפו את כל ה-motionPlan של הרכיב בעותק של התוכנית שפורסמה, שה-slotValues שלו כוללים את השינויים שלכם, עם צבעים כמערכי RGBA. עדכון חלקי של slotValues ימחק את שאר התוכנית. Nodaro עצמו וה-SDK בונים את זה בשבילכם.
{
"inputOverrides": {
"node-mg1": {
"motionPlan": {
// ...the published node's motionPlan, unchanged...
"slotValues": { "primaryColor": [0, 1, 0, 1], "nameText": "Acme Inc." }
}
}
}
}קלטים של רכיב כתובת וידאו
רכיב כתובת וידאו יכול להיות קלט של אפליקציה, כך שהאפליקציה מקבלת פרק כקישור במקום כהעלאה. הכלי get_app_inputs מחזיר את השדה כ-type: "video", והקלט השטוח הוא הקישור:
{ "inputs": { "episode": "https://www.youtube.com/watch?v=VIDEO_ID" } }- מה השדה מקבל. קישור לדף שבו נמצא סרטון, או כל קישור
httpאוhttpsציבורי אחר לסרטון. כשהקישור הוא לדף של סרטון, השרת מוריד את הסרטון, וכל קישור אחר משמש כמו שהוא. ערך שהוא לא קישור לאינטרנט, או שמצביע על כתובת פרטית, נדחה עם400 locked_fieldלפני שמשהו רץ ולפני כל חיוב. קישור לרכיב כתובת וידאו שהאפליקציה לא חושפת נדחה באותה דרך. - בדף של האפליקציה. קישור לדף של סרטון מציג את תיבת זכויות התוכן מתחת לשדה. מי שמשתמש באפליקציה מסמן את התיבה כדי לאשר את ההצהרה לקישור שלו, ושום דבר לא נמשך לפני כן. כשהקישור משתנה, הסימון מתבטל. כשרק רכיב מתכון תוכן קורא את הקישור, שום דבר לא מיובא ולא מוצגת תיבה. רכיב דיבוב מדבב את הקובץ, ולכן קישור שרכיב דיבוב קורא מורד כמו קישור שרכיב צופה בו.
- השרת מוריד את הסרטון. לפני שהרכיב הראשון רץ, השרת מושך את מה שהרכיבים שאחרי הקישור קוראים. רכיב שצופה בסרטון מקבל את הסרטון. הרכיבים תמלול וקאבר Suno מקבלים רק את האודיו. רכיב דיבוב מקבל את הסרטון, אלא אם הוא מדבב קישור משלו, מהשדה קישור מקור. רכיב מתכון תוכן מקבל את כתובת הדף, ולכן שום דבר לא נמשך בשבילו.
- סרטון ארוך צריך קטע. מאתר ש-Nodaro קורא בו קודם את אורך הסרטון, סרטון של 4 דקות או יותר, או סרטון שאי אפשר לקרוא את האורך שלו, מורד רק כקטע. ציינו את הקטע ברכיב, ב-
inputOverrides, בשניות:{ "<nodeId>": { "sectionStartSec": 600, "sectionEndSec": 1200 } }. בלי קטע, ההרצה נדחית לפני כל חיוב. גם שידור חי נדחה, וכשההורדה נכשלת, ההרצה נדחית והסיבה מצוינת. - דריסות גולמיות. ב-
inputOverrides, הקישור נכנס לשדהyoutubeUrlשל הרכיב. - זכויות תוכן. המשיכה של השרת נחשבת לייבוא של הקישור. שלחו ב-
inputOverrides, בשדהrightsConfirmationIdשל הרכיב, את האישור לקישור הזה ולהרצה הזו, שמקבלים מ-POST /v1/content-rights/confirm. אישור אחד מאפשר הורדה אחת בתוך שעה, ולכן הוא אף פעם לא נשמר עם קלטים שמורים. הרצה בלי אישור נדחית רק כשהפריסה דורשת אישור. ראו בדף ה-API זכויות תוכן. - מחיר. השלבים שאחרי הקישור, שהחיוב עליהם נקבע לפי אורך הסרטון, מתומחרים לפי דקה של ההקלטה הזו, ב-
perMinuteCredits.
קלטים של סרטון UGC
לאפליקציה שבנויה על רכיבי סרטון UGC, ב-Nodaro Cloud, יש שני קלטים משלה: כרטיס היוצר ואורך הסרטון.
כרטיס היוצר. פריט node של רכיב יוצר/ת UGC הוא כרטיס היוצר: יוצר/ת בינה מלאכותית או התמונה שלי, אישה או גבר, ובאפשרות התמונה שלי גם העלאה של התמונה של מי שמריץ את האפליקציה. הכרטיס כותב שלושה שדות של הרכיב, ולכן הרצה שולחת אותם תחת המזהה של רכיב היוצר:
{
"inputOverrides": {
"<creator node id>": {
"source": "photo",
"gender": "woman",
"photoUrl": "https://example.com/runner.jpg"
}
}
}- השדה
sourceהואsampled(לאפשרות יוצר/ת בינה מלאכותית) אוphoto. - השדה
genderהואwomanאוman. - השדה
photoUrlהוא כתובת URL ציבורית של תמונה, לאפשרות התמונה שלי בלבד.
כלי ה-MCP get_app_inputs מחזיר את הכרטיס כשלושה מפתחות: המפתח של היוצר עצמו ל-source, ואותו מפתח עם _gender ועם _photo_url, ששניהם אופציונליים. הרצת אפליקציה אף פעם לא מחכה שייבחר יוצר: היא מרנדרת יוצר אחד. הרצה באפשרות התמונה שלי שלא נותנת תמונה של מי שמריץ אותה נדחית עם 400 ugc_photo_required לפני שמשהו רץ ולפני כל חיוב.
אורך הסרטון. ההגדרה אורך הסרטון (שניות) של רכיב תסריט UGC, targetDurationSec, יכולה להיות פריט field. ההגדרה היא מחוון מ-4 עד 300 שניות, או האורכים שהאפליקציה מאפשרת ב-allowedValues, כגון [15, 20, 25]. ב-inputOverrides, שלחו אותה כמספר: 20, ולא "20". קלט שטוח מקבל את שתי הצורות.
הרצה לא יכולה לקבוע שום דבר אחר ברכיב UGC. כל שדה אחר ברכיב UGC, כגון תוצאה שנשמרה, תסריט או קליפ, נדחה עם 400 locked_field. ראו את הסעיף סרטון UGC באפליקציה בדף אפליקציות (מיני־אפליקציות).
שלב 3: בניית הטופס
// Adapt to your framework.
type FormField = {
nodeId: string
field: string
label: string // "aspectRatio" becomes "Aspect ratio"
control: "text" | "textarea" | "number" | "toggle" | "select" | "upload"
options?: Array<string | number | boolean>
defaultValue: unknown
}
async function buildForm(slug: string, baseUrl: string): Promise<FormField[]> {
const app = await fetch(`${baseUrl}/v1/app/${slug}`).then((r) => r.json())
const nodesById = new Map(app.snapshotNodes.map((n) => [n.id, n]))
const fields: FormField[] = []
const walk = (items) => {
for (const it of items ?? []) {
if (it.type === "group") walk(it.items)
if (it.type !== "field") continue
const node = nodesById.get(it.nodeId)
const current = node?.data?.[it.field]
fields.push(toFormField(it, node, current))
}
}
walk(app.snapshotSettings?.presentationSettings?.inputItems ?? [])
return fields
}- השתמשו במזהה הרכיב כמפתח של הערכים, לא בתווית. גוף הבקשה של ההרצה הוא
{ "inputOverrides": { "<nodeId>": { "<field>": value } } }. - Nodaro אוכף את
allowedValues. ערך מחוץ לרשימה נדחה עם400 validation_error. כך גם ערך מחוץ לרשימה הקבועה של שדה של הגדרה מהירה. - Nodaro אוכף את
maxLength. טקסט ארוך מהמגבלה נדחה עם400 input_too_long. הוסיפוmaxlengthלשדה הטקסט והציגו מונה תווים. בקלט של טקסט להקראה, המחיר של האפליקציה מחושב לפי המגבלה הזו. - Nodaro אוכף מגבלות של בוררים. המפרסם יכול להגביל בורר, כגון הבוררים סביבה, מצב־רוח או אדם, לחלק מהקטלוג שלו, ב-
snapshotSettings.presentationSettings.cardMeta[nodeId]. בורר עם ממד אחד משתמש ב-pickerAllowedValues, ובורר עם כמה ממדים משתמש ב-pickerAllowedValuesByField({ field: [ids] }). ערך מחוץ לרשימה נדחה עם400 validation_errorבכל הרצה של האפליקציה, כולל הרצות דרך ה-API. כדי לבטל בחירה, השמיטו את השדה או שלחו ערך ריק. אל תשלחו את ברירת המחדל של הקטלוג. - מלאו מראש את ערכי ברירת המחדל מ-
snapshotNodes[i].data, כדי שגם משתמש שלא משנה כלום יקבל הרצה הגיונית. - הציגו את
estimatedCredits, כדי שהמשתמשים יידעו מה העלות לפני שהם מריצים. כש-perMinuteCreditsאוperItemCreditsשונים מ-0, המחיר גדל עם אורך ההקלטה או עם מספר הפריטים ברשימה: הציגו חלק קבוע ועוד החלקים האלה. השדהestimatedCreditsכולל גם רינדור סופי, אם יש כזה, והשדהrunEstimatedCredits, עם החלקים שלו לדקה ולפריט, הוא מחיר ההרצה בלבד. כשהשדהmaxEstimatedCreditsמופיע, המחיר נקבע לפי מה שהמשתמש בוחר: הציגו אותו כטווח, כגון ״החל מ-5 ועד 9״. המספר שאחרי ״עד״ הוא המחיר המרבי של הרצה על פני כל השדות שהאפליקציה חושפת: לפי הערכים שהמפרסם התיר, היכן שהוא הגדיר אותם, לפי האפשרויות של כל מודל ולפי קליפ ייחוס שהמשתמש מספק, באורך המרבי שלו. גרסה שפורסמה לפני שהטווח הזה נוסף לא כוללת שדותmax*, עד שמפרסמים אותה שוב. כש-estimatedCreditsIsEstimateהואtrue, המחיר כולל אומדן של קריאת LLM. הקריאה הזו מחויבת לפי הטוקנים שהיא משתמשת בהם בפועל, ולכן הציגו את המחיר כ-~5, ולא כ-5. - הציגו את המגבלה היומית כש-
maxRunsPerUserPerDayמוגדר, כגון ״נוצלו היום 2 מתוך 5 הרצות״, כדי שתשובת429לא תפתיע.
שלב 4: התחלת הרצה ובדיקת הסטטוס שלה
התחילו את ההרצה עם אסימון:
curl -X POST https://app.nodaro.ai/v1/app/<slug>/run \
-H "Authorization: Bearer $NODARO_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"inputOverrides": {
"node-abc": { "prompt": "a cat astronaut", "aspectRatio": "16:9" }
}
}'התשובה היא 202 Accepted:
{ "executionId": "exec-uuid", "runId": "run-uuid", "status": "pending" }| שדה אופציונלי בגוף הבקשה | מה הוא עושה |
|---|---|
version | מריץ גרסה מסוימת של האפליקציה. ברירת המחדל היא הגרסה האחרונה. |
inputs | הקלטים של האפליקציה כמיפוי שטוח של שמות קלטים לערכים, כמו שה-SDK וה-CLI שולחים אותם. השדה inputOverrides מוחל מעל inputs, שדה אחר שדה, וגובר בכל שדה ששניהם קובעים. שדה מספרי, כגון מחוון שהאפליקציה חושפת, מקבל מספר או מחרוזת מספרית: הערך "0.4" מגיע כ-0.4. |
runId | משייך את ההרצה לטיוטת הרצה שיצרתם קודם. בבנייה ראשונה אפשר להתעלם ממנו. |
השדה inputOverrides יכול לקבוע גם שדות של רכיבים שהאפליקציה לא חושפת, כגון promptPrefix; ראו את הדף טקסט לפני ואחרי הפרומפט. הוא אף פעם לא יכול לשנות את היעד של רכיב ששולח החוצה או את המקום שממנו הוא מביא נתונים, כגון הכתובת של רכיב פלט Webhook, או איזה תהליך רכיב תת־תהליך מריץ: הרצה כזו נדחית עם 400 locked_field.
אפליקציה עם תוספת מחיר של היוצר. ב-Nodaro Cloud, הרצה של אפליקציה עם תוספת מחיר של היוצר נבדקת מול היתרה של מי שמריץ אותה, לפני שמשהו נכתב או רץ. היתרה צריכה לכסות את מחיר ההרצה המוצג של האפליקציה יחד עם תוספת המחיר:
- המחיר עם התוספת הוא המחיר המוצג שהתוספת חלה עליו, ועוד התשלום הקבוע, ועוד האחוז מהמחיר הזה, מעוגל כלפי מעלה לקרדיט שלם.
- התשלום הקבוע נספר במלואו, גם כשהמחיר המוצג הוא 0.
- הרצה שהיתרה לא מכסה נדחית עם
402 insufficient_credits. ההודעה מציינת את מחיר ההרצה עם התוספת, את התוספת ואת היתרה. - יוצר שמריץ את האפליקציה שלו לא משלם תוספת מחיר, וההרצה שלו לא נבדקת.
בהרצה שסביבת עבודה של ארגון משלמת עליה, סביבת העבודה משלמת את תוספת המחיר כשהארגון שלה אישר את האפליקציה. אחרת, את תוספת המחיר משלם מי שמריץ:
- הרצה של אפליקציה מאושרת נדחית עם השגיאה של סביבת העבודה עצמה,
402 budget_exceeded, כשהיתרה הפנויה בתקציב לא מכסה את המחיר המוצג יחד עם תוספת המחיר. - הרצה של אפליקציה שהארגון לא אישר נדחית עם
402 insufficient_creditsכשהיתרה של מי שמריץ לא מכסה את תוספת המחיר. על ההרצה עצמה סביבת העבודה משלמת גם במקרה הזה.
החיוב הסופי על תוספת המחיר נעשה כשההרצה מסתיימת, וממי שמשלם נגבה רק מה שיש לו באותו רגע. כלומר, לכל היותר היתרה של מי שמריץ, שאף פעם לא יורדת מתחת לאפס בגלל התוספת, או לכל היותר היתרה הפנויה בתקציב של סביבת העבודה. היוצר מקבל בדיוק את הסכום שנגבה. את השאר לא גובים מאף אחד ולא משלמים לאף אחד. ברשומת הרווחים של היוצר נשמרת תוספת המחיר שנדרשה (flatFee, percentFee) לצד מה שנגבה בפועל (totalEarned). ראו את דף ה-API סביבות עבודה וארגונים.
אחר כך בדקו את הסטטוס של ההרצה מדי כמה שניות:
curl https://app.nodaro.ai/v1/app/<slug>/runs/<runId> \
-H "Authorization: Bearer $NODARO_API_TOKEN"{
"id": "run-uuid",
"executionId": "exec-uuid",
"status": "pending|running|completed|failed",
"creditsUsed": 0, // the run's total, every attempt included
"thumbnailUrl": null, // set when the run completes
"execution": {
"status": "pending|running|completed|failed",
"nodeStates": {
"node-abc": {
"status": "completed",
"output": { // the shape depends on the node's outputType
"url": "https://.../result.png",
"imageUrl": "...",
"videoUrl": "...",
"audioUrl": "...",
"resultUrl": "...",
"text": "..."
}
}
},
"totalNodes": 5,
"completedNodes": 5,
"failedNodes": 0,
"totalCreditsUsed": 5, // this attempt's own
"errorMessage": null,
"completedAt": "2026-05-07T..."
}
}- בדקו את הסטטוס בערך כל 2 שניות. הפסיקו כש-
execution.statusהואcompletedאוfailed. - הציגו את ההתקדמות בתור
completedNodesמתוךtotalNodesבזמן שההרצה רצה. - כשההרצה נכשלת, הציגו את
execution.errorMessage.
בעל ההרצה יכול לשנות רק את השדות name, inputValues, hiddenNodes ו-nodeStates שלה, עם PATCH /v1/app/{slug}/runs/{runId}. השרת מתעלם מכל שדה אחר, ובקשת PATCH שלא כוללת אף אחד מארבעת השדות האלה נדחית עם 400 bad_request.
התשובות שמחזירות הרצה כוללות את העריכות שלכם לתוצאות ההרצה ב-nodeStateEdits, המיפוי שבקשת ה-PATCH קובעת עם nodeStates. בקשת ה-PATCH מחליפה את כל המיפוי הזה. שלחו את כל העריכות שכבר שמורות, יחד עם העריכות החדשות שלכם, אחרת עריכות שתשמיטו יאבדו.
שלב 5: הצגת התוצאה
התוצאה הראשית היא הפלט של הרכיב ש-thumbnailNodeId מציין. קבלו את הכתובת שלה מ-execution.nodeStates[thumbnailNodeId].output, ונסו את המפתחות האלה לפי הסדר: url, imageUrl, videoUrl, audioUrl, resultUrl, ואחריהם text לתוצאת טקסט, שאותה מציגים כטקסט ולא כקישור.
הציגו אותה לפי ה-outputType של הרכיב, שמגיע מ-GET /v1/nodes/{type}:
outputType | איך להציג אותה |
|---|---|
image | תמונה |
video | נגן וידאו עם פקדים |
audio | נגן אודיו עם פקדים |
text | טקסט מעוצב מראש או Markdown |
data | תצוגת JSON, או נתונים אטומים |
כש-thumbnailNodeId הוא null, השתמשו ברכיב האחרון לפי הסדר של התהליך, או הציגו כל פלט שהוא לא ריק.
רינדור סופי של הרצה
כשרינדור של רכיב החלת רשימת עריכה באפליקציה מפיק תצוגה מקדימה, הדף של האפליקציה מציג עליה את הכפתור רינדור סופי. בקוד, שלחו POST /v1/app/{slug}/runs/{runId}/render-final עם { "renderNodeId": "<render node id>" }. השרת מרנדר את הרינדור הזה במצב סופי, להרצה הזו בלבד, ואז מריץ פעם אחת כל רכיב שאחריו. שום דבר שלפני הרינדור לא רץ שוב, ולא משלמים עליו שוב.
התשובה היא 202 עם { executionId, runId, status: "pending" }. כשיש כותרת Idempotency-Key באורך של 8 תווים או יותר, התגובה לניסיון חוזר של אותה בקשה היא 200 עם "deduped": true, ושום דבר לא מתחיל.
- מי משלם. על הרינדור הסופי משלם מי שמריץ את האפליקציה, לפי המחירים של הרכיבים עצמם, כהרצה שנייה מחוץ להרצת האפליקציה. תוספת המחיר של היוצר על האפליקציה לא חלה. הרינדור הסופי לא מוסיף שורה להיסטוריית ההרצות, ולא נספר במגבלת ההרצות היומית של האפליקציה.
- הבדיקה שלכם נלקחת בחשבון. בדיקה של רכיב תוכנית עריכה ששמורה בהרצה דרך בקשת ה-
PATCHשלמעלה חלה על הרינדור הסופי. - איפה התוצאות מופיעות. כשיש לרינדור הסופי תוצאות, הנתיב
GET /v1/app/{slug}/runs/{runId}מחזיר אותן ב-execution.nodeStates, במקום המצבים של התצוגה המקדימה, ומסמן כל אחת מהן ב-"fromRenderFinal": true. ההרצה כוללת גםfinalExecution: { id, status, completedNodes, totalNodes, errorMessage, completedAt, creditsUsed }, אוnullכשלא התבקש רינדור סופי. ה-creditsUsedשל ההרצה עצמה ממשיך לשקף רק את הרצת האפליקציה. - את רכיב הידוק פרק משלימים באותה דרך. רכיב הידוק פרק שהמתג סקירה לפני רינדור מופעל בו שומר תצוגה מקדימה משלו, ולכן ציינו את הרכיב עצמו בתור
renderNodeId. הרינדור הסופי מרנדר את הפרק במצב סופי עם הבדיקה שלכם של העריכה, ואז מוסיף את הכתוביות ואת מוזיקת הרקע שלו, בלי לשלם שוב את המחיר של רכיב הידוק פרק. הרכיבים שאחריו כבר רצו על התצוגה המקדימה בהרצת האפליקציה, והם רצים שוב ברינדור הסופי. רק טייק של תצוגה מקדימה עובר רינדור סופי: טייק שכבר במצב סופי, או טייק שרץ פעם אחת לכל פריט ברשימה, נדחה עםrender_final_not_preview. - רינדור סופי שנכשל לא מוצג. השדה
finalExecutionמדווח על ה-statusועל ה-errorMessageשלו, ותצוגות ההרצה חוזרות להציג את התצוגה המקדימה.
| סטטוס | קוד | משמעות |
|---|---|---|
| 400 | not_a_render | השדה renderNodeId הוא לא רכיב רינדור של האפליקציה. |
| 400 | preview_render_nested | האפליקציה היא רכיב שמור. |
| 402 | insufficient_app_credits | הקרדיטים שלכם לא מכסים את הרינדור הסופי. |
| 404 | not_found | אין לכם הרצה כזו באפליקציה הזו. |
| 409 | continuation_not_completed | ההרצה היא טיוטה שעוד לא רצה, או שהיא לא הושלמה. |
| 409 | already_running | רינדור סופי של ההרצה הזו עדיין רץ. המזהה שלו נמצא ב-executionId. |
| 503 | run_finals_unavailable | כרגע אי אפשר לקרוא את הרינדורים הסופיים הקודמים של ההרצה. נסו שוב: שום דבר לא התחיל. |
כל דחייה מגיעה לפני שמשהו נכתב ולפני כל חיוב.
ניסיון חוזר של הרצה שנכשלה
את הרצת האפליקציה שנכשלה ממשיכים מהמקום שבו היא נעצרה, ולא מתחילים אותה מההתחלה. שלחו POST /v1/app/{slug}/runs/{runId}/resume. זה מה שהכפתור נסו שוב בדף של האפליקציה עושה. רק השלבים שההרצה לא השלימה רצים שוב. כל שלב שהיא השלימה מעביר הלאה את התוצאה שלו, ולא משלמים עליו שוב. רכיב קליפ UGC שנכשל מרנדר רק את הקליפים שהוא לא סיים.
- שלחו קודם
{ "dryRun": true, "version": <n> }. התשובה היא200עםsteps(השלבים שירוצו שוב),estimatedCredits(הסכום הצפוי לחיוב על הניסיון החוזר, כולל תוספת המחיר של היוצר),reusedClips,sufficientו-available. השדהsufficientמציין אם היתרה שלכם והקרדיטים לאפליקציות שלכם מכסים את הניסיון החוזר. השדהappCreditsAllowanceמופיע רק כשהקרדיטים לאפליקציות הם מה שעוצר אותו: הניסיון החוזר יידחה אז עםinsufficient_app_credits. שום דבר לא מתחיל. - הציגו למשתמש את השלבים ואת המחיר.
- אחרי שהמשתמש מסכים, שלחו את אותו גוף בקשה בלי
dryRun. התשובה היא202עם{ executionId, runId, status: "pending", nodeIds }. בדקו את סטטוס ההרצה מדי כמה שניות, כמו בשלב 4.
- הגרסה והקלטים של ההרצה עצמה. הניסיון החוזר מריץ את גרסת האפליקציה שההרצה הריצה, עם הקלטים שההרצה השתמשה בהם. הוא לא מקבל קלטים משלו. שלחו את השדה
"version"עם הגרסה שהממשק שלכם מריץ. בלי השדה הזה, הניסיון החוזר משתמש בגרסה האחרונה של האפליקציה. - התחלה מחדש במקום זאת. כדי להריץ עם קלטים אחרים או בגרסה הנוכחית של האפליקציה, התחילו את ההרצה מחדש: שלחו
POST /v1/app/{slug}/runעם"runId": "<runId>". כל שלב רץ שוב, ומשלמים עליו שוב. - ניסיון חוזר אחד בכל פעם. שתי לחיצות בבת אחת מתחילות ניסיון חוזר אחד: התשובה השנייה היא
200עם"deduped": true. לחיצה בזמן שהניסיון החוזר ממתין נדחית עם409 already_running, והתגובה מציינת את המזהה שלו. ניסיון חוזר של ניסיון חוזר ממשיך ממה שהניסיון האחרון השלים. - תוספת המחיר של היוצר נגבית פעם אחת, על מה שהניסיון החוזר מריץ. כמו הרצת אפליקציה, הניסיון החוזר מוציא מהקרדיטים לאפליקציות של מי שמריץ אותו.
- קרדיטים בכל הניסיונות. ה-
creditsUsedשל ההרצה הוא הסכום הכולל שלה בכל הניסיונות: מה שההרצה שנכשלה הוציאה ועוד מה שכל ניסיון חוזר הוציא. הוא מתעדכן בזמן שניסיון חוזר רץ. השדהexecution.totalCreditsUsedהוא ההוצאה של הניסיון הנוכחי בלבד. התחלה מחדש של ההרצה פותחת סכום כולל חדש. - נתוני השימוש סופרים הרצה פעם אחת. בנתוני השימוש של האפליקציה, הרצה נספרת לפי התוצאה של הניסיון האחרון שלה שהסתיים. הרצה שנכשלה ואז הושלמה בניסיון חוזר היא הרצה מוצלחת אחת. ניסיון חוזר לא מוסיף הרצה. הקרדיטים של כל ניסיון נספרים ביום שבו הניסיון הזה הסתיים.
- דרך ה-SDK, ה-CLI ו-MCP. ב-SDK הניסיון החוזר הוא
client.apps.resume, ב-CLI הואnodaro apps run-retry, ובשרת ה-MCP הואretry_app_run.
| סטטוס | קוד | משמעות |
|---|---|---|
| 400 | validation_error | מזהה הרצה או גוף בקשה פגומים, או הרצה של רכיב שמור. את ההרצה של רכיב שמור ממשיכה ההרצה שקראה לו. |
| 400 | continuation_workflow_mismatch / continuation_version_mismatch | ההרצה שמאחורי הרצת האפליקציה הריצה תהליך אחר, או גרסה אחרת שלו. זה נדיר. התחילו את ההרצה מחדש. |
| 400 | locked_field | קלט שההרצה השתמשה בו קובע שדה שהאפליקציה לא מאפשרת למי שמריץ לקבוע. |
| 400 | preview_review_required | הניסיון החוזר היה נעצר בתצוגה מקדימה, בלי הסימון שהדף של האפליקציה מוסיף. |
| 402 | insufficient_app_credits / insufficient_credits | הקרדיטים לאפליקציות או היתרה של מי שמריץ לא מכסים את הניסיון החוזר. |
| 404 | not_found | אין לכם הרצה כזו באפליקציה הזו. |
| 404 | continuation_not_found | ההרצה שמאחורי הרצת האפליקציה לא שייכת לכם. |
| 409 | app_run_version_unavailable | הגרסה שההרצה הריצה כבר לא זמינה. התחילו את ההרצה מחדש. |
| 409 | app_run_version_changed | האפליקציה בגרסה אחרת מזו של ההרצה (appVersion, runVersion). התחילו את ההרצה מחדש, או שלחו את הגרסה של ההרצה. |
| 409 | continuation_not_failed | ההרצה לא נכשלה: היא הושלמה, או שעצרו אותה. התחילו אותה מחדש. |
| 409 | already_running | ההרצה עדיין רצה (executionId), או שהתחילו אותה שוב במקום אחר. |
| 409 | continuation_subset_required | כל השלבים של ההרצה הושלמו. אין מה לנסות שוב. הסטטוס הוא 400 כשהאפליקציה לא מריצה שום שלב. |
| 503 | billing_unavailable | החיוב לא זמין לרגע בהרצה של סביבת עבודה. נסו שוב בעוד רגע. |
כשניסיון חוזר נדחה עם 409, הציעו להתחיל את ההרצה מחדש.
מחיקת הרצה
curl -X DELETE https://app.nodaro.ai/v1/app/<slug>/runs/<runId> \
-H "Authorization: Bearer $NODARO_API_TOKEN"בקשת מחיקה מעבירה את ההרצה לארכיון של המשתמש, והתגובה היא { "success": true, "archived": true }. שום דבר לא נמחק לצמיתות: אוטומציה שמוחקת את ההרצה הלא נכונה לא יכולה לגרום לאובדן הנתונים של המשתמש. אפשר לשחזר הרצה או למחוק אותה לצמיתות רק ב-Nodaro עצמו, ולכן האינטגרציה שלכם לא צריכה לנסות לעשות אף אחד מהשניים.
אימות
כל הרצה צריכה אסימון Bearer. בחרו את סוג האסימון לפי מי שאמור להיות הבעלים של ההרצות ולשלם עליהן:
| מה אתם בונים | במה להשתמש |
|---|---|
| כלי אישי, לוח בקרה של סוכנות או אוטומציה פנימית | אסימון API אישי. |
| SaaS שבו לקוחות מחברים את חשבונות ה-Nodaro שלהם | OAuth. |
| כלי ציבורי שמריץ אפליקציה בשביל מבקרים אנונימיים | אסימון API אישי. אתם משלמים, ולכן הגבילו בצד שלכם את מספר ההרצות לכל מבקר. |
אסימון API אישי. החשבון שלכם הוא הבעלים של כל הרצה ומשלם עליה. צרו את האסימון בדף הגדרות › מפתחות API בלחיצה על יצירת אסימון, העתיקו אותו מיד ושמרו אותו כסוד בשרת. האסימון תקף עד שתבטלו אותו. ראו את דף ה-API אימות.
OAuth. החשבון של כל משתמש הוא הבעלים של ההרצות שלו ומשלם עליהן. רשמו יישום מפתחים, ושלחו את המשתמש למסך ההסכמה עם ההרשאות workflows:execute ו-jobs:read. ההרשאה workflows:execute מאפשרת להתחיל הרצות, לנסות אותן שוב, לבצע את הרינדור הסופי שלהן ולשנות את השם שלהן או למחוק אותן. ההרשאה jobs:read מאפשרת לבדוק את הסטטוס של הרצה ולקבל רשימת הרצות, וגם כל אחת מההרשאות workflows:execute או assets:read לבדה מספיקה לפעולות האלה. טעינת האפליקציה עם GET /v1/app/{slug} לא דורשת הרשאה. בשרת שלכם, החליפו את הקוד באסימון גישה שתקף ל-90 ימים. אם קריאה נדחית עם 403 insufficient_scope, שלחו את המשתמש שוב דרך מסך ההסכמה עם ההרשאות הרחבות יותר.
שמירת האסימון בשרת שלכם
אסור שהאסימון יגיע לדפדפן. ה-bundlers משלבים כל משתנה סביבה עם התחילית VITE_ או NEXT_PUBLIC_ בקוד ה-JavaScript שהם מפיקים, ושם כל מי שפותח את כלי המפתחים יכול לקרוא אותו ולהוציא את הקרדיטים שלכם.
[Browser] --HTTPS--> [Your server or edge function] --HTTPS--> Nodaro API
(holds NODARO_API_TOKEN)| סטאק | איפה האסימון נשמר |
|---|---|
| Lovable עם Supabase | סוד של Supabase Edge Function: supabase secrets set NODARO_API_TOKEN=... |
| Next.js | route handler בצד השרת, עם process.env.NODARO_API_TOKEN ובלי התחילית NEXT_PUBLIC_ |
| SvelteKit, Remix, Nuxt | משתנה סביבה של השרת בלבד, כגון $env/static/private ב-SvelteKit |
| פונקציית קצה ב-Vercel או ב-Netlify | משתנה סביבה של הפרויקט שלא נחשף ללקוח |
| Cloudflare Worker | סוד של ה-Worker: wrangler secret put NODARO_API_TOKEN |
קוד הדפדפן שלכם מדבר רק עם השרת שלכם, ולכן אין צורך להוסיף את הדומיין שלכם למקורות המורשים של Nodaro. אם בכל זאת אתם קוראים ל-Nodaro מהדפדפן עם OAuth, רשמו את ה-origin שלכם בשדה מקורות מורשים של יישום המפתחים.
שגיאות
שגיאות מגיעות בצורה { "error": { "code": "...", "message": "..." } }.
| סטטוס | קוד | סיבה | מה לעשות |
|---|---|---|---|
400 | validation_error | השדה inputOverrides פגום, ערך מחוץ ל-allowedValues או מחוץ לרשימה המותרת של בורר, או slug פגום. | תקנו את גוף הבקשה. |
400 | input_too_long | קלט טקסט ארוך מה-maxLength של השדה. | קצרו את הטקסט למגבלה שהפריט מצהיר עליה. |
400 | locked_field | דריסה מציינת יעד של רכיב ששולח החוצה, כגון הכתובת של רכיב פלט Webhook, או שדה שרכיב UGC לא מקבל מהרצה. | הסירו אותה. האפליקציה קובעת לאן היא שולחת ומאיפה היא מביאה, ורכיב UGC מקבל רק את השדות של כרטיס היוצר ואת אורך הסרטון. |
400 | ugc_photo_required | רכיב יוצר/ת UGC מוגדר לאפשרות התמונה שלי, וההרצה לא נותנת תמונה של מי שמריץ אותה. | בקשו את התמונה של מי שמריץ, או העבירו את הכרטיס לאפשרות יוצר/ת בינה מלאכותית. |
401 | unauthorized | האסימון חסר, פג תוקפו או בוטל. | צרו אסימון חדש, או אשרו את הגישה שוב. |
402 | insufficient_app_credits | אין בחשבון של האסימון מספיק קרדיטים. | הוסיפו קרדיטים או שנו את התוכנית. |
402 | insufficient_credits | אפליקציה שיוצרת סרטון UGC: היתרה לא מכסה את מה שעשוי להיות משוריין לסרטון בזמן שהקליפים שלו מרונדרים. | הוסיפו קרדיטים, או צרו סרטון קצר יותר. |
402 | insufficient_credits | אפליקציה עם תוספת מחיר של היוצר: היתרה לא מכסה את מחיר ההרצה המוצג של האפליקציה יחד עם התוספת. בהרצה בסביבת עבודה של אפליקציה שהארגון לא אישר, היתרה של מי שמריץ לא מכסה את התוספת. ההרצה של היוצר עצמו לא נבדקת. | הוסיפו קרדיטים. |
402 | budget_exceeded | הרצה בסביבת עבודה של אפליקציה עם תוספת מחיר של היוצר שהארגון אישר: התקציב של סביבת העבודה לא מכסה את מחיר ההרצה המוצג של האפליקציה יחד עם התוספת. | בקשו ממנהל של סביבת העבודה להגדיל את היתרה הפנויה. |
403 | insufficient_scope | לאסימון ה-OAuth חסרה הרשאה, שמופיעה ב-missingScope. | אשרו את הגישה שוב עם ההרשאות הרחבות יותר. |
404 | not_found | ה-slug או ההרצה לא קיימים, האפליקציה הושבתה, או שההרצה שמאחורי הרצת האפליקציה לא שייכת לבעלים של ההרצה. | בדקו את ה-slug, והציגו ״האפליקציה לא זמינה״. |
429 | rate_limit_exceeded | המשתמש הגיע למגבלת ההרצות היומית של האפליקציה. | הציגו ״הגעתם למגבלה היומית״. |
500 | internal_error | תקלה בשרת. | נסו שוב פעם אחת אחרי הפסקה קצרה, ודווחו על התקלה אם היא חוזרת. |
קוד לדוגמה
Supabase Edge Function כפרוקסי לאפליקציה
// supabase/functions/nodaro-run/index.ts
import { serve } from "https://deno.land/std@0.224.0/http/server.ts"
const BASE = Deno.env.get("NODARO_BASE_URL")! // for example https://app.nodaro.ai
const TOKEN = Deno.env.get("NODARO_API_TOKEN")! // ndr_...
const SLUG = Deno.env.get("NODARO_APP_SLUG")! // my-cool-app
serve(async (req) => {
const url = new URL(req.url)
// Public probe: no token is sent, because the endpoint is public.
if (req.method === "GET" && url.pathname.endsWith("/schema")) {
const r = await fetch(`${BASE}/v1/app/${SLUG}`)
return new Response(await r.text(), { status: r.status, headers: { "content-type": "application/json" } })
}
// Start a run.
if (req.method === "POST" && url.pathname.endsWith("/run")) {
const { inputs } = await req.json()
const r = await fetch(`${BASE}/v1/app/${SLUG}/run`, {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ inputOverrides: inputs }),
})
return new Response(await r.text(), { status: r.status, headers: { "content-type": "application/json" } })
}
// Poll a run.
const m = url.pathname.match(/\/runs\/([0-9a-f-]{36})$/)
if (req.method === "GET" && m) {
const r = await fetch(`${BASE}/v1/app/${SLUG}/runs/${m[1]}`, {
headers: { "Authorization": `Bearer ${TOKEN}` },
})
return new Response(await r.text(), { status: r.status, headers: { "content-type": "application/json" } })
}
return new Response("Not found", { status: 404 })
})קוד לדפדפן: קריאה, הרצה ובדיקת סטטוס
// 1. On mount: read the schema and build the form (steps 1 to 3).
const schema = await fetch("/functions/v1/nodaro-run/schema").then((r) => r.json())
const inputItems = schema.snapshotSettings?.presentationSettings?.inputItems ?? []
// 2. On submit: send the values keyed by node id.
const start = await fetch("/functions/v1/nodaro-run/run", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
inputs: { "node-abc": { prompt: form.prompt, aspectRatio: form.aspectRatio } },
}),
}).then((r) => r.json())
// 3. Poll every 2 seconds until the run ends.
let last = start
while (last.status !== "completed" && last.execution?.status !== "completed"
&& last.status !== "failed" && last.execution?.status !== "failed") {
await new Promise((res) => setTimeout(res, 2000))
last = await fetch(`/functions/v1/nodaro-run/runs/${start.runId}`).then((r) => r.json())
}
// 4. Read the result.
const out = last.execution?.nodeStates?.[schema.thumbnailNodeId]?.output ?? {}
const heroUrl = out.url ?? out.imageUrl ?? out.videoUrl ?? out.audioUrl ?? out.resultUrl
const heroText = out.textבקוד הזה, הדפדפן שולח ערכים לפי מזהי רכיבים, ופונקציית הקצה מעבירה אותם הלאה בתור inputOverrides.
רשימת בדיקות לפני יצירת קוד הממשק
- הקריאה ל-
GET {BASE}/v1/app/{slug}החזירה את מבנה הקלטים ואת ערכי ברירת המחדל. - לכל
nodeIdב-inputItems, אתם יודעים מה ה-typeשלו לפיsnapshotNodes. - לכל סוג רכיב, הקריאה ל-
GET {BASE}/v1/nodes/{type}נתנה לכם את הקטגוריה, את ה-outputTypeואת המודלים שלו. - בניתם את רשימת השדות של הטופס לפי הכללים של שלב 2.
- אתם יודעים מהו רכיב התוצאה,
thumbnailNodeIdאו הרכיב האחרון, ומה ה-outputTypeשלו. - יש לכם אסימון API אישי או פרטי גישה של יישום מפתחים.
- הסוד נשמר בשרת שלכם.
שאלות נפוצות
איך מטמיעים מיני־אפליקציה של Nodaro באתר שלי?
פתחו את הדף מיני־אפליקציות, בחרו בלשונית המיני־אפליקציות שלי ולחצו על הכפתור הטמעה בכרטיס של האפליקציה. הוסיפו את הדומיין של האתר שלכם תחת דומיינים מורשים להטמעה, לחצו על הכפתור העתקת קוד הטמעה והדביקו את ה-iframe בדף שלכם. ההטמעה חסומה עד שמוסיפים לפחות דומיין אחד.
באילו נקודות קצה צריך להשתמש כדי לבנות ממשק משלי למיני־אפליקציה?
כדי לגלות מהם הקלטים של מיני־אפליקציה, קוראים לשתי נקודות קצה ציבוריות: GET /v1/app/{slug} ו-GET /v1/nodes/{type}. אחר כך מתחילים הרצה עם POST /v1/app/{slug}/run ובודקים את הסטטוס שלה מדי כמה שניות עם GET /v1/app/{slug}/runs/{runId}. שתי נקודות הקצה האלה דורשות אסימון.
האם Nodaro קורא לשרת שלי כשהרצה של מיני־אפליקציה מסתיימת?
לא. הרצה של מיני־אפליקציה לא שולחת אליכם קריאה חוזרת. בדקו את הסטטוס של ההרצה בערך כל 2 שניות, עד שהוא completed או failed.
מי משלם על ההרצות של מיני־אפליקציה מוטמעת?
על ההרצות של מיני־אפליקציה מוטמעת משלם החשבון שההרצה פועלת בשמו. עם אסימון API אישי, החשבון שלכם משלם על כל הרצה. עם OAuth, החשבון של כל משתמש משלם על ההרצות שלו.
האם אפשר למחוק הרצה של מיני־אפליקציה דרך ה-API?
כן. בקשת DELETE להרצה של מיני־אפליקציה מעבירה אותה לארכיון, ואפשר לשחזר אותה ב-Nodaro. אפשר לשחזר הרצה או למחוק אותה לצמיתות רק ב-Nodaro עצמו.
דפים קשורים
הטמעות
אפליקציות (מיני־אפליקציות)
אפליקציות OAuth
אימות
SDK ל-TypeScript
עודכן לאחרונה