[{"data":1,"prerenderedAt":1742},["ShallowReactive",2],{"search-en":3,"content-en-developer\u002Fplugin-send-providers":4,"surround-en-\u002Fdeveloper\u002Fplugin-send-providers":1733},[],{"id":5,"title":6,"body":7,"description":1726,"extension":1727,"meta":1728,"navigation":277,"path":1729,"seo":1730,"stem":1731,"__hash__":1732},"content_en\u002F3.developer\u002F49.plugin-send-providers.md","Authoring a Send Provider",{"type":8,"value":9,"toc":1714},"minimark",[10,24,31,34,76,83,114,119,189,214,246,874,893,897,908,920,1051,1058,1084,1088,1091,1273,1278,1310,1329,1335,1339,1375,1379,1386,1419,1429,1468,1479,1482,1489,1504,1507,1511,1528,1654,1657,1661,1710],[11,12,13,14,18,19,23],"p",{},"A ",[15,16,17],"strong",{},"send provider"," is an emailing provider that is not Owlat's own MTA: an ESP, a relay, an SMTP service. Owlat treats every one of them as a ",[20,21,22],"em",{},"bundle"," — a send path, a feedback path, a sending-domain identity, capability declarations and a credential form that travel together — and it accepts that bundle at either of two tiers with the same contract.",[11,25,26,27,30],{},"Provider N+1 is a ",[15,28,29],{},"package",". Nothing in routing, dispatch, retries, the deliverability fallback, the ramp controller, the measurement plane or the credentials UI is edited to add one.",[11,32,33],{},"Start with the scaffold:",[35,36,41],"pre",{"className":37,"code":38,"language":39,"meta":40,"style":40},"language-sh shiki shiki-themes github-light github-dark-dimmed","owlat plugins create acme-relay --name @acme\u002Fowlat-relay --template send-provider\n","sh","",[42,43,44],"code",{"__ignoreMap":40},[45,46,49,53,57,60,63,67,70,73],"span",{"class":47,"line":48},"line",1,[45,50,52],{"class":51},"sOLd2","owlat",[45,54,56],{"class":55},"s-HuK"," plugins",[45,58,59],{"class":55}," create",[45,61,62],{"class":55}," acme-relay",[45,64,66],{"class":65},"sviXB"," --name",[45,68,69],{"class":55}," @acme\u002Fowlat-relay",[45,71,72],{"class":65}," --template",[45,74,75],{"class":55}," send-provider\n",[11,77,78,79,82],{},"That emits a complete, composing bundle: all three executable halves, every capability field this tier may declare, a credential form joined to the variables it writes, a host-verifiable webhook contract, and a suite per half. What is left is your vendor's part, marked ",[42,80,81],{},"TODO"," at the line it belongs on.",[11,84,85,86,89,90,93,94,97,98,101,102,105,106,109,110,113],{},"The scaffold lands ",[20,87,88],{},"inside"," the Owlat checkout you run it in — ",[42,91,92],{},"examples\u002Fplugins\u002F\u003Cid>"," unless you pass ",[42,95,96],{},"--dir",", and never outside the workspace. That is why the emitted ",[42,99,100],{},"package.json"," is ",[42,103,104],{},"private"," and its dependencies are ",[42,107,108],{},"workspace:*"," and ",[42,111,112],{},"catalog:"," specifiers: the package is wired to that checkout until you move it out. Publishing it is the last step of moving out, and the emitted README lists the four things that step is.",[115,116,118],"h2",{"id":117},"the-bundle","The bundle",[120,121,122,138],"table",{},[123,124,125],"thead",{},[126,127,128,132,135],"tr",{},[129,130,131],"th",{},"Half",[129,133,134],{},"What it is",[129,136,137],{},"Required",[139,140,141,157,167,178],"tbody",{},[126,142,143,147,154],{},[144,145,146],"td",{},"Catalog declaration",[144,148,149,150,153],{},"The ",[42,151,152],{},"sendTransports"," contribution in your manifest: capabilities, credential form, retry delays",[144,155,156],{},"Always",[126,158,159,162,165],{},[144,160,161],{},"Send module",[144,163,164],{},"One network attempt per call, returning a typed outcome",[144,166,156],{},[126,168,169,172,175],{},[144,170,171],{},"Feedback webhook",[144,173,174],{},"Parse-only: verified bytes in, feedback facts out",[144,176,177],{},"Only if your provider reports delivery events",[126,179,180,183,186],{},[144,181,182],{},"Sending-domain identity",[144,184,185],{},"The provider conversation about a customer's own domain",[144,187,188],{},"Only if your provider verifies domains through an API",[11,190,191,192,195,196,199,200,203,204,195,207,209,210,213],{},"The two optional halves are not accompanied by a boolean. Declaring a ",[42,193,194],{},"webhook"," ",[15,197,198],{},"is"," the catalog's ",[42,201,202],{},"hasProviderFeedback: true"," for your kind, and declaring a ",[42,205,206],{},"domainIdentity",[15,208,198],{}," its ",[42,211,212],{},"domainVerification: 'api'",". One fact, stated once: a boolean beside it could only ever disagree with the module that implements it.",[11,215,216,217,220,221,223,224,229,230,233,234,237,238,241,242,245],{},"Below is the manifest a bundle is built around — the scaffold's ",[42,218,219],{},"src\u002Fmanifest.ts"," with its vendor ",[42,222,81],{},"s answered. It grows the file the ",[225,226,228],"a",{"href":227},"\u002Fdeveloper\u002Fplugin-authoring","minimal manifest"," starts, so it imports only the capability constant it adds; ",[42,231,232],{},"definePlugin"," is already in scope. The scaffold derives every variable name and header from your plugin id (",[42,235,236],{},"acme-relay"," ⇒ ",[42,239,240],{},"PLUGIN_ACME_RELAY_API_KEY",", ",[42,243,244],{},"x-acme-relay-signature","), which is what the names here are.",[35,247,251],{"className":248,"code":249,"language":250,"meta":40,"style":40},"language-ts shiki shiki-themes github-light github-dark-dimmed","import { PLUGIN_SEND_TRANSPORT_CAPABILITY } from '@owlat\u002Fplugin-kit';\n\nexport const acmeRelayPlugin = definePlugin({\n    id: 'acme-relay',\n    version: '1.0.0',\n    capabilities: [PLUGIN_SEND_TRANSPORT_CAPABILITY],\n    \u002F\u002F Deployment-wide: the plugin is off until both are set. The signing secret\n    \u002F\u002F belongs HERE and not on the transport — without it the feedback route can\n    \u002F\u002F verify nothing and answers every delivery 503.\n    flag: {\n        default: false,\n        requiredEnvVars: ['ACME_RELAY_ENABLED', 'PLUGIN_ACME_RELAY_WEBHOOK_SECRET'],\n    },\n    contributes: {\n        sendTransports: [\n            {\n                id: 'relay',\n                label: 'Acme Relay',\n                module: { exportPath: '.\u002Fconvex\u002Ftransport' },\n                retryDelays: [1_000, 5_000],\n                \u002F\u002F THIS TRANSPORT's own configuration: resolved per named instance\n                \u002F\u002F (`PLUGIN_ACME_RELAY_API_KEY__EU` for `plugin.acme-relay.relay#eu`) and\n                \u002F\u002F handed to `send` keyed by the base name.\n                requiredEnvVars: ['PLUGIN_ACME_RELAY_API_KEY'],\n                optionalEnvVars: ['PLUGIN_ACME_RELAY_REGION'],\n                credentialFields: [\n                    {\n                        kind: 'secret',\n                        key: 'apiKey',\n                        label: 'API key',\n                        required: true,\n                        envVar: 'PLUGIN_ACME_RELAY_API_KEY',\n                    },\n                    {\n                        kind: 'select',\n                        key: 'region',\n                        label: 'Sending region',\n                        options: [\n                            { value: 'eu', label: 'Europe' },\n                            { value: 'us', label: 'United States' },\n                        ],\n                        default: 'eu',\n                        envVar: 'PLUGIN_ACME_RELAY_REGION',\n                    },\n                ],\n                \u002F\u002F `no` is the only value this tier may declare.\n                supportsCustomReturnPath: 'no',\n                messageIdSource: 'provider',\n                deduplicatesOnIdempotencyKey: false,\n                \u002F\u002F Declaring a webhook IS `hasProviderFeedback: true` for this kind.\n                webhook: {\n                    module: { exportPath: '.\u002Fconvex\u002Fwebhook' },\n                    signature: {\n                        header: 'x-acme-relay-signature',\n                        algorithm: 'hmac-sha256',\n                        encoding: 'hex',\n                        secretEnvVar: 'PLUGIN_ACME_RELAY_WEBHOOK_SECRET',\n                        \u002F\u002F REQUIRED: without replay provisions a captured request verifies\n                        \u002F\u002F forever. The host signs `\u003Ctimestamp>.\u003CrawBody>`.\n                        replay: { timestampHeader: 'x-acme-relay-timestamp', toleranceSeconds: 300 },\n                    },\n                },\n                \u002F\u002F Declaring an identity IS `domainVerification: 'api'` for this kind.\n                domainIdentity: { module: { exportPath: '.\u002Fconvex\u002FdomainIdentity' } },\n            },\n        ],\n    },\n});\n","ts",[42,252,253,272,279,301,313,324,336,343,349,355,361,372,388,394,400,406,412,423,434,446,462,468,474,480,491,502,508,514,525,536,547,558,568,574,579,589,599,609,615,632,647,653,663,672,677,683,689,700,711,721,727,733,744,750,761,772,783,793,799,805,822,827,833,839,851,857,863,868],{"__ignoreMap":40},[45,254,255,259,263,266,269],{"class":47,"line":48},[45,256,258],{"class":257},"s7YZ4","import",[45,260,262],{"class":261},"sYgZi"," { PLUGIN_SEND_TRANSPORT_CAPABILITY } ",[45,264,265],{"class":257},"from",[45,267,268],{"class":55}," '@owlat\u002Fplugin-kit'",[45,270,271],{"class":261},";\n",[45,273,275],{"class":47,"line":274},2,[45,276,278],{"emptyLinePlaceholder":277},true,"\n",[45,280,282,285,288,291,294,298],{"class":47,"line":281},3,[45,283,284],{"class":257},"export",[45,286,287],{"class":257}," const",[45,289,290],{"class":65}," acmeRelayPlugin",[45,292,293],{"class":257}," =",[45,295,297],{"class":296},"sPO5f"," definePlugin",[45,299,300],{"class":261},"({\n",[45,302,304,307,310],{"class":47,"line":303},4,[45,305,306],{"class":261},"    id: ",[45,308,309],{"class":55},"'acme-relay'",[45,311,312],{"class":261},",\n",[45,314,316,319,322],{"class":47,"line":315},5,[45,317,318],{"class":261},"    version: ",[45,320,321],{"class":55},"'1.0.0'",[45,323,312],{"class":261},[45,325,327,330,333],{"class":47,"line":326},6,[45,328,329],{"class":261},"    capabilities: [",[45,331,332],{"class":65},"PLUGIN_SEND_TRANSPORT_CAPABILITY",[45,334,335],{"class":261},"],\n",[45,337,339],{"class":47,"line":338},7,[45,340,342],{"class":341},"sDN9O","    \u002F\u002F Deployment-wide: the plugin is off until both are set. The signing secret\n",[45,344,346],{"class":47,"line":345},8,[45,347,348],{"class":341},"    \u002F\u002F belongs HERE and not on the transport — without it the feedback route can\n",[45,350,352],{"class":47,"line":351},9,[45,353,354],{"class":341},"    \u002F\u002F verify nothing and answers every delivery 503.\n",[45,356,358],{"class":47,"line":357},10,[45,359,360],{"class":261},"    flag: {\n",[45,362,364,367,370],{"class":47,"line":363},11,[45,365,366],{"class":261},"        default: ",[45,368,369],{"class":65},"false",[45,371,312],{"class":261},[45,373,375,378,381,383,386],{"class":47,"line":374},12,[45,376,377],{"class":261},"        requiredEnvVars: [",[45,379,380],{"class":55},"'ACME_RELAY_ENABLED'",[45,382,241],{"class":261},[45,384,385],{"class":55},"'PLUGIN_ACME_RELAY_WEBHOOK_SECRET'",[45,387,335],{"class":261},[45,389,391],{"class":47,"line":390},13,[45,392,393],{"class":261},"    },\n",[45,395,397],{"class":47,"line":396},14,[45,398,399],{"class":261},"    contributes: {\n",[45,401,403],{"class":47,"line":402},15,[45,404,405],{"class":261},"        sendTransports: [\n",[45,407,409],{"class":47,"line":408},16,[45,410,411],{"class":261},"            {\n",[45,413,415,418,421],{"class":47,"line":414},17,[45,416,417],{"class":261},"                id: ",[45,419,420],{"class":55},"'relay'",[45,422,312],{"class":261},[45,424,426,429,432],{"class":47,"line":425},18,[45,427,428],{"class":261},"                label: ",[45,430,431],{"class":55},"'Acme Relay'",[45,433,312],{"class":261},[45,435,437,440,443],{"class":47,"line":436},19,[45,438,439],{"class":261},"                module: { exportPath: ",[45,441,442],{"class":55},"'.\u002Fconvex\u002Ftransport'",[45,444,445],{"class":261}," },\n",[45,447,449,452,455,457,460],{"class":47,"line":448},20,[45,450,451],{"class":261},"                retryDelays: [",[45,453,454],{"class":65},"1_000",[45,456,241],{"class":261},[45,458,459],{"class":65},"5_000",[45,461,335],{"class":261},[45,463,465],{"class":47,"line":464},21,[45,466,467],{"class":341},"                \u002F\u002F THIS TRANSPORT's own configuration: resolved per named instance\n",[45,469,471],{"class":47,"line":470},22,[45,472,473],{"class":341},"                \u002F\u002F (`PLUGIN_ACME_RELAY_API_KEY__EU` for `plugin.acme-relay.relay#eu`) and\n",[45,475,477],{"class":47,"line":476},23,[45,478,479],{"class":341},"                \u002F\u002F handed to `send` keyed by the base name.\n",[45,481,483,486,489],{"class":47,"line":482},24,[45,484,485],{"class":261},"                requiredEnvVars: [",[45,487,488],{"class":55},"'PLUGIN_ACME_RELAY_API_KEY'",[45,490,335],{"class":261},[45,492,494,497,500],{"class":47,"line":493},25,[45,495,496],{"class":261},"                optionalEnvVars: [",[45,498,499],{"class":55},"'PLUGIN_ACME_RELAY_REGION'",[45,501,335],{"class":261},[45,503,505],{"class":47,"line":504},26,[45,506,507],{"class":261},"                credentialFields: [\n",[45,509,511],{"class":47,"line":510},27,[45,512,513],{"class":261},"                    {\n",[45,515,517,520,523],{"class":47,"line":516},28,[45,518,519],{"class":261},"                        kind: ",[45,521,522],{"class":55},"'secret'",[45,524,312],{"class":261},[45,526,528,531,534],{"class":47,"line":527},29,[45,529,530],{"class":261},"                        key: ",[45,532,533],{"class":55},"'apiKey'",[45,535,312],{"class":261},[45,537,539,542,545],{"class":47,"line":538},30,[45,540,541],{"class":261},"                        label: ",[45,543,544],{"class":55},"'API key'",[45,546,312],{"class":261},[45,548,550,553,556],{"class":47,"line":549},31,[45,551,552],{"class":261},"                        required: ",[45,554,555],{"class":65},"true",[45,557,312],{"class":261},[45,559,561,564,566],{"class":47,"line":560},32,[45,562,563],{"class":261},"                        envVar: ",[45,565,488],{"class":55},[45,567,312],{"class":261},[45,569,571],{"class":47,"line":570},33,[45,572,573],{"class":261},"                    },\n",[45,575,577],{"class":47,"line":576},34,[45,578,513],{"class":261},[45,580,582,584,587],{"class":47,"line":581},35,[45,583,519],{"class":261},[45,585,586],{"class":55},"'select'",[45,588,312],{"class":261},[45,590,592,594,597],{"class":47,"line":591},36,[45,593,530],{"class":261},[45,595,596],{"class":55},"'region'",[45,598,312],{"class":261},[45,600,602,604,607],{"class":47,"line":601},37,[45,603,541],{"class":261},[45,605,606],{"class":55},"'Sending region'",[45,608,312],{"class":261},[45,610,612],{"class":47,"line":611},38,[45,613,614],{"class":261},"                        options: [\n",[45,616,618,621,624,627,630],{"class":47,"line":617},39,[45,619,620],{"class":261},"                            { value: ",[45,622,623],{"class":55},"'eu'",[45,625,626],{"class":261},", label: ",[45,628,629],{"class":55},"'Europe'",[45,631,445],{"class":261},[45,633,635,637,640,642,645],{"class":47,"line":634},40,[45,636,620],{"class":261},[45,638,639],{"class":55},"'us'",[45,641,626],{"class":261},[45,643,644],{"class":55},"'United States'",[45,646,445],{"class":261},[45,648,650],{"class":47,"line":649},41,[45,651,652],{"class":261},"                        ],\n",[45,654,656,659,661],{"class":47,"line":655},42,[45,657,658],{"class":261},"                        default: ",[45,660,623],{"class":55},[45,662,312],{"class":261},[45,664,666,668,670],{"class":47,"line":665},43,[45,667,563],{"class":261},[45,669,499],{"class":55},[45,671,312],{"class":261},[45,673,675],{"class":47,"line":674},44,[45,676,573],{"class":261},[45,678,680],{"class":47,"line":679},45,[45,681,682],{"class":261},"                ],\n",[45,684,686],{"class":47,"line":685},46,[45,687,688],{"class":341},"                \u002F\u002F `no` is the only value this tier may declare.\n",[45,690,692,695,698],{"class":47,"line":691},47,[45,693,694],{"class":261},"                supportsCustomReturnPath: ",[45,696,697],{"class":55},"'no'",[45,699,312],{"class":261},[45,701,703,706,709],{"class":47,"line":702},48,[45,704,705],{"class":261},"                messageIdSource: ",[45,707,708],{"class":55},"'provider'",[45,710,312],{"class":261},[45,712,714,717,719],{"class":47,"line":713},49,[45,715,716],{"class":261},"                deduplicatesOnIdempotencyKey: ",[45,718,369],{"class":65},[45,720,312],{"class":261},[45,722,724],{"class":47,"line":723},50,[45,725,726],{"class":341},"                \u002F\u002F Declaring a webhook IS `hasProviderFeedback: true` for this kind.\n",[45,728,730],{"class":47,"line":729},51,[45,731,732],{"class":261},"                webhook: {\n",[45,734,736,739,742],{"class":47,"line":735},52,[45,737,738],{"class":261},"                    module: { exportPath: ",[45,740,741],{"class":55},"'.\u002Fconvex\u002Fwebhook'",[45,743,445],{"class":261},[45,745,747],{"class":47,"line":746},53,[45,748,749],{"class":261},"                    signature: {\n",[45,751,753,756,759],{"class":47,"line":752},54,[45,754,755],{"class":261},"                        header: ",[45,757,758],{"class":55},"'x-acme-relay-signature'",[45,760,312],{"class":261},[45,762,764,767,770],{"class":47,"line":763},55,[45,765,766],{"class":261},"                        algorithm: ",[45,768,769],{"class":55},"'hmac-sha256'",[45,771,312],{"class":261},[45,773,775,778,781],{"class":47,"line":774},56,[45,776,777],{"class":261},"                        encoding: ",[45,779,780],{"class":55},"'hex'",[45,782,312],{"class":261},[45,784,786,789,791],{"class":47,"line":785},57,[45,787,788],{"class":261},"                        secretEnvVar: ",[45,790,385],{"class":55},[45,792,312],{"class":261},[45,794,796],{"class":47,"line":795},58,[45,797,798],{"class":341},"                        \u002F\u002F REQUIRED: without replay provisions a captured request verifies\n",[45,800,802],{"class":47,"line":801},59,[45,803,804],{"class":341},"                        \u002F\u002F forever. The host signs `\u003Ctimestamp>.\u003CrawBody>`.\n",[45,806,808,811,814,817,820],{"class":47,"line":807},60,[45,809,810],{"class":261},"                        replay: { timestampHeader: ",[45,812,813],{"class":55},"'x-acme-relay-timestamp'",[45,815,816],{"class":261},", toleranceSeconds: ",[45,818,819],{"class":65},"300",[45,821,445],{"class":261},[45,823,825],{"class":47,"line":824},61,[45,826,573],{"class":261},[45,828,830],{"class":47,"line":829},62,[45,831,832],{"class":261},"                },\n",[45,834,836],{"class":47,"line":835},63,[45,837,838],{"class":341},"                \u002F\u002F Declaring an identity IS `domainVerification: 'api'` for this kind.\n",[45,840,842,845,848],{"class":47,"line":841},64,[45,843,844],{"class":261},"                domainIdentity: { module: { exportPath: ",[45,846,847],{"class":55},"'.\u002Fconvex\u002FdomainIdentity'",[45,849,850],{"class":261}," } },\n",[45,852,854],{"class":47,"line":853},65,[45,855,856],{"class":261},"            },\n",[45,858,860],{"class":47,"line":859},66,[45,861,862],{"class":261},"        ],\n",[45,864,866],{"class":47,"line":865},67,[45,867,393],{"class":261},[45,869,871],{"class":47,"line":870},68,[45,872,873],{"class":261},"});\n",[11,875,876,877,880,881,884,885,888,889,892],{},"Everything executable lives at a ",[42,878,879],{},"module.exportPath"," your package exports, and each one is imported as that module's ",[15,882,883],{},"default export"," — as is the manifest, at your package root. Codegen verifies each path without running it; the host imports the module only when a send, a delivery or an identity check actually needs it. Export by name as well if you like, but a module with ",[20,886,887],{},"only"," a named export composes into a registry entry whose module is ",[42,890,891],{},"undefined",", and the first send is the thing that finds out. The scaffold emits both.",[115,894,896],{"id":895},"the-provider-checklist-at-both-tiers","The provider checklist, at both tiers",[11,898,899,900,903,904,907],{},"The same six steps, whichever tier you are on. ",[15,901,902],{},"Nothing else"," — enforced by the conformance suites, which iterate catalog kinds rather than a list, and by the ",[42,905,906],{},"lint:providers"," ratchet, which fails any comparison against a provider-kind literal outside an adapter folder.",[11,909,910,911,915,916,919],{},"The core column below is a summary. ",[225,912,914],{"href":913},"\u002Fdeveloper\u002Fproviders#send-email--the-provider-n1-checklist","Send Providers → the provider-N+1 checklist"," is the canonical core-tier list, with the exact file paths, the compile-time guards behind each step and the one ",[15,917,918],{},"optional"," step this table does not carry.",[120,921,922,935],{},[123,923,924],{},[126,925,926,929,932],{},[129,927,928],{},"#",[129,930,931],{},"Core kind",[129,933,934],{},"Plugin kind",[139,936,937,962,989,1008,1021,1037],{},[126,938,939,942,957],{},[144,940,941],{},"1",[144,943,944,945,948,949,952,953,956],{},"Catalog entry in ",[42,946,947],{},"packages\u002Fshared\u002Fsrc\u002FsendProviderCatalog.ts"," (kind, capabilities, credential fields — the union widens itself; the field vocabulary lives in ",[42,950,951],{},"sendProviderCatalogTypes.ts"," and the form descriptors in ",[42,954,955],{},"sendProviderCredentialFields.ts",")",[144,958,959,961],{},[42,960,152],{}," contribution in the package manifest, with the same capability fields",[126,963,964,967,978],{},[144,965,966],{},"2",[144,968,969,970,973,974,977],{},"Env keys in ",[42,971,972],{},"lib\u002Fenv.ts"," and the ",[42,975,976],{},".env"," examples",[144,979,980,981,984,985,988],{},"Env keys declared in the contribution's ",[42,982,983],{},"requiredEnvVars"," (the host asserts presence; ",[42,986,987],{},"lint:env"," untouched)",[126,990,991,994,1001],{},[144,992,993],{},"3",[144,995,996,997,1000],{},"Adapter folder ",[42,998,999],{},"lib\u002FsendProviders\u002F\u003Ckind>\u002F"," (a completeness guard forces it)",[144,1002,1003,1004,1007],{},"Send ",[42,1005,1006],{},"module"," export, provenance-verified and pinned by a mapped-type guard in the generated registry",[126,1009,1010,1013,1016],{},[144,1011,1012],{},"4",[144,1014,1015],{},"Webhook adapter in the feedback registry, if the provider reports feedback",[144,1017,1018,1020],{},[42,1019,194],{}," module export, if the provider reports feedback",[126,1022,1023,1026,1032],{},[144,1024,1025],{},"5",[144,1027,1028,1029],{},"Domain-identity provider in the registry, if verification is ",[42,1030,1031],{},"api",[144,1033,1034,1036],{},[42,1035,206],{}," module export, if the provider verifies domains",[126,1038,1039,1042,1045],{},[144,1040,1041],{},"6",[144,1043,1044],{},"Docs page section",[144,1046,1047,1048],{},"Package README; listed in ",[42,1049,1050],{},"plugins.config.ts",[11,1052,1053,1054,1057],{},"No routing, dispatch, ramp, measurement or setup-wizard edit is ",[15,1055,1056],{},"required"," at either tier. If you find yourself making one, the capability you need is missing from the contract — that is a change to the contract, not to your bundle.",[11,1059,1060,1061,1064,1065,1068,1069,1072,1073,1076,1077,109,1080,1083],{},"The one thing a core kind may ",[15,1062,1063],{},"optionally"," add is UI wording: ",[42,1066,1067],{},"apps\u002Fweb"," carries ",[42,1070,1071],{},"Partial"," override maps for a transport's label, its DNS guidance paragraph and its picker copy (step 7 on the canonical list). A kind with no row renders from its catalog entry — nothing breaks and nothing is blank — so write one only when this surface should word your provider differently. A ",[15,1074,1075],{},"plugin kind has no equivalent step at all",": those maps are keyed by core kind, and a bundle's own ",[42,1078,1079],{},"label",[42,1081,1082],{},"credentialFields"," are what describe it.",[115,1085,1087],{"id":1086},"capability-semantics","Capability semantics",[11,1089,1090],{},"Every field below is optional and every one has the same fail-closed default a core entry that omitted it would get, so a manifest written against an older contract composes exactly as it did.",[120,1092,1093,1106],{},[123,1094,1095],{},[126,1096,1097,1100,1103],{},[129,1098,1099],{},"Field",[129,1101,1102],{},"What a bundled transport may declare",[129,1104,1105],{},"What it promises",[139,1107,1108,1133,1165,1190,1208,1230,1245,1259],{},[126,1109,1110,1115,1120],{},[144,1111,1112],{},[42,1113,1114],{},"supportsCustomReturnPath",[144,1116,1117],{},[42,1118,1119],{},"no",[144,1121,1122,1123,109,1126,1129,1130,1132],{},"Whether Owlat may choose the envelope sender. ",[42,1124,1125],{},"yes",[42,1127,1128],{},"probe"," need a VERP local part signed with a deployment secret a bundled module is never handed, so ",[42,1131,1119],{}," is the only value this tier has — and the honest one.",[126,1134,1135,1140,1148],{},[144,1136,1137],{},[42,1138,1139],{},"messageIdSource",[144,1141,1142,241,1145],{},[42,1143,1144],{},"provider",[42,1146,1147],{},"composed",[144,1149,1150,1151,1154,1155,1157,1158,1161,1162,1164],{},"Where the id your ",[42,1152,1153],{},"send"," returns comes from: your own API (",[42,1156,1144],{},"), or the RFC 5322 ",[42,1159,1160],{},"Message-ID"," Owlat already minted and you echoed back (",[42,1163,1147],{},"). Every feedback event is joined on it.",[126,1166,1167,1172,1178],{},[144,1168,1169],{},[42,1170,1171],{},"deduplicatesOnIdempotencyKey",[144,1173,1174,241,1176],{},[42,1175,555],{},[42,1177,369],{},[144,1179,1180,1181,195,1183,195,1186,1189],{},"Whether the same request may be sent twice under one key without delivering twice. Declaring ",[42,1182,555],{},[15,1184,1185],{},"requires",[42,1187,1188],{},"buildSystemMailExtras"," carrying that key into your request; the host refuses the composition otherwise, because a claim without the wiring turns a double delivery into a \"safe\" retry of a password reset.",[126,1191,1192,1197,1202],{},[144,1193,1194],{},[42,1195,1196],{},"hasProviderFeedback",[144,1198,1199],{},[20,1200,1201],{},"derived",[144,1203,1204,1205,1207],{},"True exactly when the contribution declares a ",[42,1206,194],{},".",[126,1209,1210,1215,1219],{},[144,1211,1212],{},[42,1213,1214],{},"domainVerification",[144,1216,1217],{},[20,1218,1201],{},[144,1220,1221,1223,1224,241,1226,1229],{},[42,1222,1031],{}," exactly when the contribution declares a ",[42,1225,206],{},[42,1227,1228],{},"none"," otherwise.",[126,1231,1232,1237,1242],{},[144,1233,1234],{},[42,1235,1236],{},"acceptanceSemantics",[144,1238,1239],{},[20,1240,1241],{},"not declarable",[144,1243,1244],{},"Custody of an in-flight message. Only Owlat's own MTA declares it; a third party's timeout is always ambiguous.",[126,1246,1247,1252,1256],{},[144,1248,1249],{},[42,1250,1251],{},"tagsFeedbackProvenance",[144,1253,1254],{},[20,1255,1241],{},[144,1257,1258],{},"Says our own MTA stamped the report on its way out of our own infrastructure. Never true of a third party.",[126,1260,1261,1266,1270],{},[144,1262,1263],{},[42,1264,1265],{},"setupProbe",[144,1267,1268],{},[20,1269,1241],{},[144,1271,1272],{},"Names an exported validator in host code, which a manifest cannot add to.",[1274,1275,1277],"h3",{"id":1276},"configuration-and-named-instances","Configuration and named instances",[11,1279,1280,1282,1283,1286,1287,1290,1291,1294,1295,1297,1298,1301,1302,1305,1306,1309],{},[42,1281,983],{}," on the contribution is what ",[15,1284,1285],{},"one instance"," of your transport needs. Declaring it is what makes named instances possible: a deployment can run ",[42,1288,1289],{},"plugin.acme-relay.relay#eu"," reading ",[42,1292,1293],{},"PLUGIN_ACME_RELAY_API_KEY__EU",", and the host hands your ",[42,1296,1153],{}," exactly that instance's values, keyed by the base name your manifest wrote. Read them from the ",[42,1299,1300],{},"config"," argument — a module that reads ",[42,1303,1304],{},"process.env"," resolves the ",[20,1307,1308],{},"deployment-default"," instance's credentials whichever id the send was addressed to.",[11,1311,1312,1313,1316,1317,1320,1321,1324,1325,1328],{},"Names are held to one rule: ",[42,1314,1315],{},"PLUGIN_","-prefixed, uppercase, no ",[42,1318,1319],{},"__"," (which is the instance separator) and no trailing ",[42,1322,1323],{},"_",". The plugin's own ",[42,1326,1327],{},"flag.requiredEnvVars"," is a different scope — a deployment-wide switch read unsuffixed — and the two lists may not overlap.",[11,1330,1331,1334],{},[42,1332,1333],{},"optionalEnvVars"," is refused without at least one required variable: a transport whose whole configuration is optional has no per-instance credential to resolve, so every named instance of it would be graded against an empty requirement list.",[1274,1336,1338],{"id":1337},"the-credential-form","The credential form",[11,1340,1341,1343,1344,1347,1348,241,1351,241,1354,241,1357,241,1360,1363,1364,1367,1368,1371,1372,1374],{},[42,1342,1082],{}," are typed descriptors in the same vocabulary the platform's ",[42,1345,1346],{},"settingsSchema"," uses — ",[42,1349,1350],{},"string",[42,1352,1353],{},"secret",[42,1355,1356],{},"number",[42,1358,1359],{},"boolean",[42,1361,1362],{},"select"," — so one renderer draws your credential the way it draws a core provider's. They are ",[15,1365,1366],{},"descriptive only",": nothing there decides what a send reads. Every field's ",[42,1369,1370],{},"envVar"," must be a variable this transport declared, matched to the field's own ",[42,1373,1056],{},", which is what keeps a rendered form from asking for a variable no send reads or omitting one that gates the transport.",[115,1376,1378],{"id":1377},"webhook-security-expectations","Webhook security expectations",[11,1380,1381,1382,1385],{},"Your feedback module ",[15,1383,1384],{},"parses",". It never decides whether a request is authentic. By the time it runs, the host has already:",[1387,1388,1389,1393,1400,1403,1410,1413,1416],"ol",{},[1390,1391,1392],"li",{},"spent a per-plugin rate-limit token — the one write an unproven caller causes, and first because a limiter that does not record cannot limit;",[1390,1394,1395,1396,1399],{},"resolved the route by plugin id (",[42,1397,1398],{},"POST \u002Fwebhooks\u002Fplugin\u002F\u003CpluginId>","), answering 404 for an unknown one before a byte of the body is read;",[1390,1401,1402],{},"bounded the body's size;",[1390,1404,1405,1406,1409],{},"recomputed your declared HMAC over ",[42,1407,1408],{},"\u003Ctimestamp>.\u003CrawBody>"," in constant time and compared it against the header you named;",[1390,1411,1412],{},"refused a timestamp further from now than your declared tolerance;",[1390,1414,1415],{},"rechecked the operator's capability grant under its own audited operation;",[1390,1417,1418],{},"refused a delivery whose signature digest it has already accepted.",[11,1420,1421,1425,1426,1428],{},[225,1422,1424],{"href":1423},"\u002Fdeveloper\u002Fplugin-contributions#feedback-webhook","Contribution Reference → Feedback webhook"," is the normative list of what the ",[42,1427,194],{}," block must carry — the fields, the tolerance ceiling, which of the two variable scopes the secret belongs in, and how many webhooks a plugin may declare. It is the page to write your manifest against; this section is why each of those rules exists and what it costs to get wrong:",[1430,1431,1432,1441,1450,1456,1462],"ul",{},[1390,1433,1434,1440],{},[15,1435,1436,1439],{},[42,1437,1438],{},"signature"," is required."," A webhook without one fails manifest validation. This endpoint is unauthenticated and internet-facing by design; an unverified one would be an open write path into the delivery record.",[1390,1442,1443,1449],{},[15,1444,1445,1448],{},[42,1446,1447],{},"replay"," provisions are required too."," An HMAC over the body alone proves origin and nothing else — the same captured bytes verify forever. Binding a timestamp into the signed string and bounding its age is what makes a delivery happen once. The ceiling on that window is the kit's, not yours; pick the smallest one your provider's retries tolerate, well under it.",[1390,1451,1452,1455],{},[15,1453,1454],{},"The signing secret gates the plugin, not the transport."," Without it the route can verify nothing and answers every delivery 503 — and a run of non-2xx is what makes a provider deactivate your endpoint — so the plugin must not be enableable without it. That is why it is declared in the scope a deployment reads unsuffixed rather than beside your per-instance credential.",[1390,1457,1458,1461],{},[15,1459,1460],{},"The route surface is keyed by plugin id",", so a second webhook in the same plugin could never be addressed, and a manifest declaring one is refused rather than silently ignored.",[1390,1463,1464,1467],{},[15,1465,1466],{},"Raw payloads are not retained unless you ask."," A third party's payload may carry recipient content this deployment never asked to keep, so retention is your explicit decision rather than the pipeline's default.",[11,1469,1470,1471,1474,1475,1478],{},"Inside the module: return the empty array for a console verification ping and for event kinds Owlat does not consume — a 400 would make your provider redeliver them forever — and ",[15,1472,1473],{},"throw"," on a body you cannot read. Read an event's timestamp only for the kinds you ",[20,1476,1477],{},"do"," consume: an engagement event you ignore may omit the field entirely, and validating it above your kind switch takes the whole batch down with it. Import no Node builtins: the module is loaded by the HTTP router.",[115,1480,182],{"id":1481},"sending-domain-identity",[11,1483,1484,1485,1488],{},"Your identity module reports ",[15,1486,1487],{},"observations",", never a verdict: whether the provider has confirmed ownership, its verdicts on the published SPF and DKIM records, the DKIM selectors it signs under and the SPF mechanisms it needs authorised. The host derives the status from those, so \"verified\" means the same thing at every relay tier and a module cannot report a domain verified while telling us its DKIM is invalid. The freshness bound on that proof is a host constant, not a manifest field.",[11,1490,1491,1492,1495,1496,1499,1500,1503],{},"Distinguish the three outcomes, because the host's write rules differ for each: ",[42,1493,1494],{},"ok"," is the only one that refreshes the proof's age, ",[42,1497,1498],{},"auth_failed"," condemns a credential without overwriting the DNS verdicts already stored, and ",[42,1501,1502],{},"unavailable"," — which a thrown error is also read as — changes nothing but the retry.",[11,1505,1506],{},"Return your DKIM selectors whenever you know them. An empty list means \"we cannot describe this domain's signing identity\", which the dual-transport alignment pre-flight reads as a hold on the ramp rather than as an opened gate.",[115,1508,1510],{"id":1509},"what-the-scaffold-gives-you","What the scaffold gives you",[11,1512,1513,1514,1516,1517,109,1520,1523,1524,1527],{},"Every template writes the same three-file package skeleton — ",[42,1515,100],{}," (private, with the export map your manifest's module paths are checked against), ",[42,1518,1519],{},"tsconfig.json",[42,1521,1522],{},"vitest.config.ts",". On top of that, ",[42,1525,1526],{},"owlat plugins create \u003Cid> --template send-provider"," emits:",[120,1529,1530,1540],{},[123,1531,1532],{},[126,1533,1534,1537],{},[129,1535,1536],{},"File",[129,1538,1539],{},"What it holds",[139,1541,1542,1552,1562,1571,1581,1594,1604,1614,1624,1634,1644],{},[126,1543,1544,1549],{},[144,1545,1546],{},[42,1547,1548],{},"README.md",[144,1550,1551],{},"What is done, what is left, the environment variables an operator sets, and how to move the package out of the checkout and publish it",[126,1553,1554,1559],{},[144,1555,1556],{},[42,1557,1558],{},"src\u002Findex.ts",[144,1560,1561],{},"The package's root export: the manifest, and nothing else",[126,1563,1564,1568],{},[144,1565,1566],{},[42,1567,219],{},[144,1569,1570],{},"The data-only declaration: capabilities, credential form, signature contract",[126,1572,1573,1578],{},[144,1574,1575],{},[42,1576,1577],{},"src\u002FenvNames.ts",[144,1579,1580],{},"Every environment variable name, declared once and read by both the manifest and the modules",[126,1582,1583,1588],{},[144,1584,1585],{},[42,1586,1587],{},"src\u002Fconvex\u002Ftransport.ts",[144,1589,1590,1591,1593],{},"The send path: one attempt, credentials from ",[42,1592,1300],{},", statuses mapped onto the host's retry vocabulary",[126,1595,1596,1601],{},[144,1597,1598],{},[42,1599,1600],{},"src\u002Fconvex\u002Fwebhook.ts",[144,1602,1603],{},"The feedback path: parse only, isolate-safe",[126,1605,1606,1611],{},[144,1607,1608],{},[42,1609,1610],{},"src\u002Fconvex\u002FdomainIdentity.ts",[144,1612,1613],{},"The sending-domain identity: observations only, three distinguishable outcomes",[126,1615,1616,1621],{},[144,1617,1618],{},[42,1619,1620],{},"src\u002F__tests__\u002Fmanifest.test.ts",[144,1622,1623],{},"The manifest validates and keeps the two variable scopes apart",[126,1625,1626,1631],{},[144,1627,1628],{},[42,1629,1630],{},"src\u002F__tests__\u002Ftransport.test.ts",[144,1632,1633],{},"Credentials come from the instance configuration; the status mapping holds",[126,1635,1636,1641],{},[144,1637,1638],{},[42,1639,1640],{},"src\u002F__tests__\u002Fwebhook.test.ts",[144,1642,1643],{},"The four feedback facts, the acknowledged ping, the unreadable body",[126,1645,1646,1651],{},[144,1647,1648],{},[42,1649,1650],{},"src\u002F__tests__\u002FdomainIdentity.test.ts",[144,1652,1653],{},"Observations, and a rejected credential distinguished from an outage",[11,1655,1656],{},"The emitted bundle is not a sketch: it composes, and Owlat's own conformance gate drives the generator's output — unedited — through the shipped routing, dispatch, feedback and identity modules on every CI run.",[115,1658,1660],{"id":1659},"where-to-go-next","Where to go next",[1430,1662,1663,1673,1684,1703],{},[1390,1664,1665,1669,1670,1672],{},[225,1666,1668],{"href":1667},"\u002Fdeveloper\u002Fplugin-contributions","Contribution Reference"," — the ",[42,1671,152],{}," bucket beside every other bucket, with the module contracts in full.",[1390,1674,1675,1679,1680,1683],{},[225,1676,1678],{"href":1677},"\u002Fdeveloper\u002Fplugin-capabilities","Capabilities & Trust"," — what ",[42,1681,1682],{},"send:transport"," grants and how an operator revokes it.",[1390,1685,1686,1690,1691,241,1694,241,1697,241,1700,1207],{},[225,1687,1689],{"href":1688},"\u002Fdeveloper\u002Fplugin-cli","The owlat plugins CLI"," — ",[42,1692,1693],{},"create",[42,1695,1696],{},"add",[42,1698,1699],{},"remove",[42,1701,1702],{},"codegen",[1390,1704,1705,1709],{},[225,1706,1708],{"href":1707},"\u002Fdeveloper\u002Fproviders","Send Providers"," — the core tier's catalog, and the per-provider capability table.",[1711,1712,1713],"style",{},"html pre.shiki code .sOLd2, html code.shiki .sOLd2{--shiki-default:#6F42C1;--shiki-dark:#F69D50}html pre.shiki code .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}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 .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .sPO5f, html code.shiki .sPO5f{--shiki-default:#6F42C1;--shiki-dark:#DCBDFB}html pre.shiki code .sDN9O, html code.shiki .sDN9O{--shiki-default:#6A737D;--shiki-dark:#768390}",{"title":40,"searchDepth":274,"depth":274,"links":1715},[1716,1717,1718,1722,1723,1724,1725],{"id":117,"depth":274,"text":118},{"id":895,"depth":274,"text":896},{"id":1086,"depth":274,"text":1087,"children":1719},[1720,1721],{"id":1276,"depth":281,"text":1277},{"id":1337,"depth":281,"text":1338},{"id":1377,"depth":274,"text":1378},{"id":1481,"depth":274,"text":182},{"id":1509,"depth":274,"text":1510},{"id":1659,"depth":274,"text":1660},"The send-provider bundle contract, the two-tier provider checklist, capability semantics, and what the host guarantees about your feedback webhook.","md",{},"\u002Fdeveloper\u002Fplugin-send-providers",{"title":6,"description":1726},"3.developer\u002F49.plugin-send-providers","oQhqW2Jf0kzgUsVEu2B_r2lCRUegcMeEO831wuOKrkY",[1734,1738],{"title":1735,"path":1736,"stem":1737,"children":-1},"Plugin Troubleshooting","\u002Fdeveloper\u002Fplugin-troubleshooting","3.developer\u002F48.plugin-troubleshooting",{"title":1739,"path":1740,"stem":1741,"children":-1},"Authentication","\u002Fdeveloper\u002Fauthentication","3.developer\u002F5.authentication",1786915092728]