Example: Create a GitHub Issue or Linear Ticket from Email
This walkthrough shows how to forward emails sent to bugs@mail.yourcompany.com
into a GitHub repository or Linear team as new tickets. The email subject
becomes the ticket title and the body becomes the description.
The same pattern works for any tracker with a JSON HTTP API: Jira,
ServiceNow, Zendesk, Asana, Notion. Replace the target URL, the
Authorization header, and the payload_template to match your provider.
Overview
Sender → SMTP → Conduit → GitHub or Linear API → New ticket
Two webhook fields do the work:
- Custom headers carries the
Authorizationheader to the tracker's API. - Payload template reshapes the parsed email into the JSON body the API expects.
Both fields are set in the webhook edit form in the web UI.
Prerequisites
- A Conduit account. See Getting Started.
- A domain verified in Conduit, or use of the shared public domain. See Using a Custom Domain.
- A GitHub personal access token with
issues: writeon the target repo, or a Linear API key plus your team ID.
Step 1: Get a tracker credential
GitHub
- Open
github.com→ your profile menu → Settings → Developer settings → Personal access tokens → Fine-grained tokens. - Click Generate new token. Scope it to the single repository that should receive issues.
- Under Repository permissions, set Issues to Read and write.
- Click Generate token and copy the
github_pat_...value. It is shown once.
Linear
- Open
linear.app→ Settings → API → Personal API keys. - Click Create new key, label it (e.g. "Conduit email-to-issue"), and
copy the
lin_api_...value. - Note the team ID of the team you want issues created in. The simplest
way is to run the following query against Linear's GraphQL API:
The response contains every team'scurl -s https://api.linear.app/graphql \ -H "Authorization: lin_api_..." \ -H "Content-Type: application/json" \ -d '{"query": "{ teams { nodes { id name } } }"}'id.
Step 2: Create the Conduit webhook
GitHub
Using the web UI
- Go to Webhooks (
/app/webhooks) and click + New. - Set the address to
bugs@mail.yourcompany.com. - Set the target URL to
https://api.github.com/repos/yourorg/yourrepo/issues. - Click Create webhook.
- On the webhook detail page, click Edit.
- In the Custom headers field, enter:
Authorization: Bearer github_pat_... - In the Payload template field, enter:
{"title": "{{.Subject}}", "body": "From: {{.From}}\n\n{{.Text}}"} - Click Save changes.
Using the API
POST /api/v1/webhooks
Authorization: Bearer <conduit_access_token>
Content-Type: application/json
{
"address": "bugs@mail.yourcompany.com",
"target_url": "https://api.github.com/repos/yourorg/yourrepo/issues",
"custom_headers": {
"Authorization": "Bearer github_pat_..."
},
"payload_template": "{\"title\": \"{{.Subject}}\", \"body\": \"From: {{.From}}\\n\\n{{.Text}}\"}"
}
The rendered body for a real email looks like:
{
"title": "Login button broken on Safari",
"body": "From: alice@example.com\n\nClicking the login button on the homepage does nothing in Safari 17."
}
GitHub's Create an issue endpoint accepts this directly and responds with the new issue's URL.
Linear
Linear uses a single GraphQL endpoint, so the template wraps the email in a mutation.
Using the web UI
- Go to Webhooks (
/app/webhooks) and click + New. - Set the address to
bugs@mail.yourcompany.com. - Set the target URL to
https://api.linear.app/graphql. - Click Create webhook.
- On the webhook detail page, click Edit.
- In the Custom headers field, enter:
(Linear API keys are passed without aAuthorization: lin_api_...Bearerprefix.) - In the Payload template field, enter:
{"query": "mutation IssueCreate($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { identifier url } } }", "variables": {"input": {"teamId": "YOUR_TEAM_ID", "title": "{{.Subject}}", "description": "From: {{.From}}\n\n{{.Text}}"}}} - Click Save changes.
Using the API
POST /api/v1/webhooks
Authorization: Bearer <conduit_access_token>
Content-Type: application/json
{
"address": "bugs@mail.yourcompany.com",
"target_url": "https://api.linear.app/graphql",
"custom_headers": {
"Authorization": "lin_api_..."
},
"payload_template": "{\"query\": \"mutation IssueCreate($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { identifier url } } }\", \"variables\": {\"input\": {\"teamId\": \"YOUR_TEAM_ID\", \"title\": \"{{.Subject}}\", \"description\": \"From: {{.From}}\\n\\n{{.Text}}\"}}}"
}
Linear API keys are passed without a Bearer prefix (Authorization: lin_api_...).
Step 3: Test the integration
Using the web UI
Open the webhook detail page (/app/webhooks/{id}) and click Simulate.
Conduit sends a synthetic payload through the template to GitHub or Linear.
A new issue should appear in your tracker within a few seconds.
Or send a real test email
swaks \
--to bugs@mail.yourcompany.com \
--from you@example.com \
--server mx.conduit.email \
--port 25 \
--header "Subject: Test ticket from Conduit" \
--body "This should appear as a new issue in the tracker."
A new issue should appear in the GitHub repo or Linear team within a few seconds.
Step 4: Review delivery logs
Open the webhook's Logs page (/app/webhooks/{id}/logs) to see the
status of every delivery attempt. See Delivery Logs for
field definitions and filters.
Common errors:
| Status | Cause | Resolution |
|---|---|---|
401 from GitHub |
Token lacks issues: write on the repo. |
Recreate the token with the correct scope. |
404 from GitHub |
Repo path in target_url is wrong, or the token cannot see the repo. |
Confirm the URL and the token's repository scope. |
200 from Linear with success: false in the body |
The teamId is invalid or the field shape is wrong. |
Re-run the team-listing query in Step 1; check that the GraphQL response contains the team. |
422 from GitHub |
The rendered payload is not valid JSON, often because the email subject contained a " or \. |
See Escaping caveats below. |
Escaping caveats
payload_template is a Go text/template rendered against the parsed email
and inserted verbatim into the request body. Conduit does not JSON-escape
template values for you, so an email subject like He said "hi" will produce
invalid JSON when interpolated into a "title": "{{.Subject}}" field.
Two practical mitigations:
- Trust your senders. If the address is internal-only and protected by an SMTP security policy requiring DKIM from your own domain, malformed input is unlikely.
- Sanitize at an intermediary. Point
target_urlat a small handler you own (a Cloud Function, a Worker) that re-serializes the payload safely before forwarding to GitHub or Linear. The handler can also enrich the ticket (assignees, labels, project boards).
Restricting senders
bugs@mail.yourcompany.com is a public address; without controls anyone can
file an issue in your tracker. Attach an SMTP security
policy that requires SPF or DKIM from your own
domain, or restrict source IPs to your own egress range. The mechanics are
the same as in the Slack example.
A rate limit on the webhook is also worth setting so that an email loop or a misconfigured forwarder cannot create hundreds of tickets in a minute. Set the Rate limit field in the webhook edit form to the maximum number of emails to accept per minute.
Next steps
- Webhook Payload Reference. Full template variable list and signature-verification details.
- SMTP Security Policy. Lock the address down to trusted senders.
- Delivery Logs. Diagnose failures.