xref: /expo/docs/pages/eas/webhooks.mdx (revision dfd15ebd)
1---
2title: Webhooks
3---
4
5import { ConfigClassic } from '~/components/plugins/ConfigSection';
6import { Collapsible } from '~/ui/components/Collapsible';
7
8EAS can alert you as soon as your build or submission has completed via a webhook. Webhooks need to be configured per-project, so if you want to be alerted for both `@johndoe/awesomeApp` and `@johndoe/coolApp`, you need to run `eas webhook:create` in each directory.
9
10<ConfigClassic>
11
12Webhooks function almost exactly the same for both EAS Build and the classic `expo build` system, _except_ that for `expo build` webhooks, you'll use `expo-cli` to interact with them, and **not** `eas-cli`. For `expo build` webhooks, you'll use:
13
14- `expo webhooks [path]`: List all webhooks for a project
15- `expo webhooks:add [path]`: Add a webhook to a project
16- `expo webhooks:remove [path]`: Delete a webhook
17- `expo webhooks:update [path]`: Update an existing webhook
18
19</ConfigClassic>
20
21After running `eas webhook:create`, you'll be prompted to choose the webhook event type (unless you provide the `--event BUILD|SUBMIT` parameter). Next, provide the webhook URL (or specify it with the `--url` flag) that handles HTTP POST requests. Additionally, you'll have to input a webhook signing secret, if you have not already provided it with the `--secret` flag. It must be at least 16 characters long, and it will be used to calculate the signature of the request body which we send as the value of the `expo-signature` HTTP header. You can use the signature to verify a webhook request is genuine (example code below).
22
23EAS calls your webhook using an HTTP POST request. All the data is passed in the request body. EAS sends the data as a JSON object.
24
25Additionally, we send an `expo-signature` HTTP header with the hash signature of the payload. You can use this signature to verify the authenticity of the request. The signature is a hex-encoded HMAC-SHA1 digest of the request body, using your webhook secret as the HMAC key.
26
27> If you want to test the above webhook locally, you have to use a service like [ngrok](https://ngrok.com/docs) to forward `localhost:8080` via a tunnel and make it publicly accessible with the URL `ngrok` gives you.
28
29You can always change your webhook URL and/or webhook secret using `eas webhook:update --id WEBHOOK_ID`. You can find the webhook ID by running `eas webhook:list`. If you would like us to stop sending requests to your webhook, run `eas webhook:delete` and choose the webhook from the list.
30
31## Webhook payload
32
33<Collapsible summary="Build webhook payload">
34
35The build webhook payload looks something like this:
36
37```json
38{
39  "id": "147a3212-49fd-446f-b4e3-a6519acf264a",
40  "parentBuildId": "75ac0be7-0d90-46d5-80ec-9423fa0aaa6b", // available for build retries
41  "appId": "bc0a82de-65a5-4497-ad86-54ff1f53edf7",
42  "initiatingUserId": "d1041496-1a59-423a-8caf-479bb978203a",
43  "cancelingUserId": null, // available for canceled builds
44  "platform": "android", // or "ios"
45  "status": "errored", // or: "finished", "canceled"
46  "artifacts": {
47    "buildUrl": "https://expo.dev/artifacts/eas/wyodu9tua2ZuKKiaJ1Nbkn.aab", // available for successful builds
48    "logsS3KeyPrefix": "production/f9609423-5072-4ea2-a0a5-c345eedf2c2a"
49  },
50  "metadata": {
51    "appName": "example",
52    "username": "dsokal",
53    "workflow": "managed",
54    "appVersion": "1.0.2",
55    "appBuildVersion": "123",
56    "cliVersion": "0.37.0",
57    "sdkVersion": "41.0.0",
58    "buildProfile": "production",
59    "distribution": "store",
60    "appIdentifier": "com.expo.example",
61    "gitCommitHash": "564b61ebdd403d28b5dc616a12ce160b91585b5b",
62    "gitCommitMessage": "Add home screen",
63    "runtimeVersion": "1.0.2",
64    "channel": "default", // available for EAS Update
65    "releaseChannel": "default", // available for legacy updates
66    "reactNativeVersion": "0.60.0",
67    "appBuildVersion": "6",
68    "trackingContext": {
69      "platform": "android",
70      "account_id": "7c34cbf1-efd4-4964-84a1-c13ed297aaf9",
71      "dev_client": false,
72      "project_id": "bc0a82de-65a5-4497-ad86-54ff1f53edf7",
73      "tracking_id": "a3fdefa7-d129-42f2-9432-912050ab0f10",
74      "project_type": "managed",
75      "dev_client_version": "0.6.2"
76    },
77    "credentialsSource": "remote",
78    "isGitWorkingTreeDirty": false,
79    "message": "release build", // message attached to the build
80    "runFromCI": false
81  },
82  "metrics": {
83    "memory": 895070208,
84    "buildEndTimestamp": 1637747861168,
85    "totalDiskReadBytes": 692224,
86    "buildStartTimestamp": 1637747834445,
87    "totalDiskWriteBytes": 14409728,
88    "cpuActiveMilliseconds": 12117.540078,
89    "buildEnqueuedTimestamp": 1637747792476,
90    "totalNetworkEgressBytes": 355352,
91    "totalNetworkIngressBytes": 78781667
92  },
93  // available for failed builds
94  "error": {
95    "message": "Unknown error. Please see logs.",
96    "errorCode": "UNKNOWN_ERROR"
97  },
98  "createdAt": "2021-11-24T09:53:01.155Z",
99  "enqueuedAt": "2021-11-24T09:53:01.155Z",
100  "provisioningStartedAt": "2021-11-24T09:54:01.155Z",
101  "workerStartedAt": "2021-11-24T09:54:11.155Z",
102  "completedAt": "2021-11-24T09:57:42.715Z",
103  "updatedAt": "2021-11-24T09:57:42.715Z",
104  "expirationDate": "2021-12-24T09:53:01.155Z",
105  "priority": "high", // or: "normal", "low"
106  "resourceClass": "android-n2-1.3-12",
107  "actualResourceClass": "android-n2-1.3-12",
108  "maxRetryTimeMinutes": 3600 // max retry time for failed/canceled builds
109}
110```
111
112</Collapsible>
113
114<Collapsible summary="Submit webhook payload">
115
116The submit webhook payload looks something like this:
117
118```json
119{
120  "id": "0374430d-7776-44ad-be7d-8513629adc54",
121  "parentSubmissionId": "75ac0be7-0d90-46d5-80ec-9423fa0aaa6b", // available for submission retries
122  "appId": "23c0e405-d282-4399-b280-5689c3e1ea85",
123  "archiveUrl": "http://archive.url/abc.apk",
124  "initiatingUserId": "7bee4c21-3eaa-4011-a0fd-3678b6537f47",
125  "cancelingUserId": null, // available for canceled submissions
126  "turtleBuildId": "8c84111e-6d39-449c-9895-071d85fd3e61", // available when submitting a build from EAS
127  "platform": "android", // or "ios"
128  "status": "errored", // or: "finished", "canceled"
129  "submissionInfo": {
130    // available for failed submissions
131    "error": {
132      "message": "Android version code needs to be updated",
133      "errorCode": "SUBMISSION_SERVICE_ANDROID_OLD_VERSION_CODE_ERROR"
134    },
135    "logsUrl": "https://submission-service-logs.s3-us-west-1.amazonaws.com/production/submission_728aa20b-f7a9-4da7-9b64-39911d427b19.txt"
136  },
137  "createdAt": "2021-11-24T10:15:32.822Z",
138  "updatedAt": "2021-11-24T10:17:32.822Z",
139  "completedAt": "2021-11-24T10:17:32.822Z",
140  "maxRetryTimeMinutes": 3600 // max retry time for failed/canceled submissions
141}
142```
143
144</Collapsible>
145
146## Webhook server
147
148Here's an example of how you can implement your server:
149
150```javascript
151const crypto = require('crypto');
152const express = require('express');
153const bodyParser = require('body-parser');
154const safeCompare = require('safe-compare');
155
156const app = express();
157app.use(bodyParser.text({ type: '*/*' }));
158app.post('/webhook', (req, res) => {
159  const expoSignature = req.headers['expo-signature'];
160  // process.env.SECRET_WEBHOOK_KEY has to match SECRET value set with `eas webhook:create` command
161  const hmac = crypto.createHmac('sha1', process.env.SECRET_WEBHOOK_KEY);
162  hmac.update(req.body);
163  const hash = `sha1=${hmac.digest('hex')}`;
164  if (!safeCompare(expoSignature, hash)) {
165    res.status(500).send("Signatures didn't match!");
166  } else {
167    // do something here, like send a notification to Slack!
168    // console.log(req.body);
169    res.send('OK!');
170  }
171});
172app.listen(8080, () => console.log('Listening on port 8080'));
173```
174