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, for example Jira,
ServiceNow, Zendesk, Asana and Notion. Replace the target URL, the
Authorization header, and the payload_template for 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 API of the tracker. - Payload template converts the parsed email into the JSON body that the API expects.
You set both fields in the webhook edit form in the web UI.
Prerequisites
- A Conduit account. See Getting Started.
- A domain that is verified in Conduit, or 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. Give it access to one repository only: the repository that receives the issues.
- In Repository permissions, set Issues to Read and write.
- Click Generate token and copy the
github_pat_...value. GitHub shows it one time only.
Linear
- Open
linear.app→ Settings → API → Personal API keys. - Click Create new key, label it, for example "Conduit email-to-issue",
and copy the
lin_api_...value. - Write down the team ID of the team for the new issues. The easiest method
is this query against the Linear GraphQL API:
The response carries thecurl -s https://api.linear.app/graphql \ -H "Authorization: lin_api_..." \ -H "Content-Type: application/json" \ -d '{"query": "{ teams { nodes { id name } } }"}'idof every team.
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."
}
The GitHub Create an issue endpoint accepts this body directly. It answers with the URL of the new issue.
Linear
Linear has one GraphQL endpoint, so the template puts the email inside 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:
A Linear API key needs noAuthorization: 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}}\"}}}"
}
A Linear API key needs no 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 appears in your tracker after 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 becomes a new issue in the tracker."
A new issue appears in the GitHub repository or the Linear team after a few seconds.
Step 4: Review delivery logs
Open the Logs page of the webhook (/app/webhooks/{id}/logs) to see the
status of every delivery attempt. For the field definitions and the filters, see
Delivery Logs.
Common errors:
| Status | Cause | What to do |
|---|---|---|
401 from GitHub |
The token has no issues: write permission on the repository. |
Create the token again with the correct permission. |
404 from GitHub |
The repository path in target_url is wrong, or the token cannot see the repository. |
Make sure that the URL and the repository permission of the token are correct. |
200 from Linear with success: false in the body |
The teamId is wrong, or the field shape is wrong. |
Run the team query in Step 1 again. Make sure that the GraphQL response contains the team. |
422 from GitHub |
The rendered payload is not valid JSON. A " or a \ in the email subject is the usual cause. |
See Escaping caveats. |
Escaping caveats
payload_template is a Go text/template. Conduit renders it against the parsed
email and puts the result into the request body without a change. Conduit does
not JSON-escape the template values.
CAUTION: An email subject such as
He said "hi"produces invalid JSON in a"title": "{{.Subject}}"field.
There are two practical answers:
- Trust your senders. Keep the address internal, and protect it with an SMTP security policy that requires DKIM from your own domain. Malformed input is then unlikely.
- Sanitize in a service of your own. Point
target_urlat a small handler that you own, for example a Cloud Function or a Worker. The handler serializes the payload safely and then forwards it to GitHub or Linear. It can also add assignees, labels and project boards to the ticket.
Restricting senders
bugs@mail.yourcompany.com is a public address. Without a control, anybody can
file an issue in your tracker. Attach an
SMTP security policy that requires SPF or DKIM from
your own domain. Or limit the source IP addresses to your own egress range. The
steps are the same as in the
Slack example.
Also set a rate limit on the webhook. An email loop, or a forwarder with wrong settings, then cannot create hundreds of tickets in one minute. Set the Rate limit field in the webhook edit form to the maximum emails per minute.
Next steps
- Webhook Payload Reference. The full list of template variables, and the signature verification.
- SMTP Security Policy. Limit the address to trusted senders.
- Delivery Logs. Find the cause of a failure.