|
| 1 | +--- |
| 2 | +status: new |
| 3 | +--- |
| 4 | + |
| 5 | +# Partner Push API |
| 6 | + |
| 7 | +This page is for **Eitri partners** that need to send push notifications to the devices of an Eitri Shopping store using Eitri's infrastructure (the `push-notification-eitri-shop-api` service). |
| 8 | + |
| 9 | +The flow is a standard OAuth 2.0 *client credentials* exchange: |
| 10 | + |
| 11 | +1. The internal Eitri team generates a **`client_id` / `client_secret`** pair for your integration, bound to a specific store. |
| 12 | +2. You exchange those credentials for an **access token** at `POST /v1/oauth/token`. |
| 13 | +3. You use that token to call one of the delivery endpoints: |
| 14 | + - `POST /v1/partner/push` — free-form push, with the title, body and target devices defined by you. |
| 15 | + - `POST /v1/partner/push/order-status` — order status change push, where you send only the order and Eitri builds and fires the notification. |
| 16 | + |
| 17 | +**Base URL (production):** `https://api.eitri.tech/push-notification-eitri-shop-api` |
| 18 | + |
| 19 | +--- |
| 20 | + |
| 21 | +## 1. Requesting credentials |
| 22 | + |
| 23 | +**Credentials are not self-service.** There is no public sign-up endpoint: they are created by an Eitri administrator through an ADMIN-protected route. |
| 24 | + |
| 25 | +Open a request with the internal Eitri team (your commercial contact or the support channel) providing: |
| 26 | + |
| 27 | +| Information | Description | |
| 28 | +| --- | --- | |
| 29 | +| Store / environment (`envKey`) | Identifier of the store environment the pushes will target | |
| 30 | +| `label` | Name of the partner or integration that will use the credential (e.g. `crm-acme`) | |
| 31 | +| Required scopes | Currently the only available scope is `push:send` (applied by default) | |
| 32 | +| Technical owner | E-mail of whoever will receive and store the secret | |
| 33 | + |
| 34 | +Once created, **the Eitri team provides you with the `client_id` and the `client_secret`** — that pair is what you use to authenticate at `POST /v1/oauth/token` and get the push access token. |
| 35 | + |
| 36 | +!!! warning "The `clientSecret` is handed over only once" |
| 37 | + Eitri stores only a hash of the secret — it cannot be retrieved later. Keep it in a secret manager (Vault, AWS Secrets Manager, etc.). If it is lost or leaked, ask the Eitri team for a **rotation**: a new secret is generated and the previous one stops working immediately. |
| 38 | + |
| 39 | +### Credential lifecycle |
| 40 | + |
| 41 | +All the operations below are performed by the internal Eitri team, upon request: |
| 42 | + |
| 43 | +- **Creation** — generates a `client_id` and `client_secret` for a store. |
| 44 | +- **Rotation** — generates a new secret for the same `client_id` (invalidates the previous one). |
| 45 | +- **Scope update** — changes the set of scopes assigned to the credential. |
| 46 | +- **Revocation** — marks the credential as `revoked`. Already-issued tokens stop working on the next call, since the credential status is checked on every request. |
| 47 | + |
| 48 | +A credential is always bound to **a single store**. If you serve multiple stores, request one credential per store. |
| 49 | + |
| 50 | +--- |
| 51 | + |
| 52 | +## 2. Getting the access token |
| 53 | + |
| 54 | +`POST /v1/oauth/token` |
| 55 | + |
| 56 | +Public endpoint (no authentication beyond the credentials themselves). The supported `grant_type` is `client_credentials`. |
| 57 | + |
| 58 | +Credentials can be sent in two ways: |
| 59 | + |
| 60 | +**a) In the request body** |
| 61 | + |
| 62 | +```bash |
| 63 | +curl --request POST \ |
| 64 | + --url https://api.eitri.tech/push-notification-eitri-shop-api/v1/oauth/token \ |
| 65 | + --header 'Content-Type: application/json' \ |
| 66 | + --data '{ |
| 67 | + "grant_type": "client_credentials", |
| 68 | + "client_id": "pk_live_2f1c...", |
| 69 | + "client_secret": "s3cr3t..." |
| 70 | + }' |
| 71 | +``` |
| 72 | + |
| 73 | +**b) Via HTTP Basic** (`Authorization: Basic base64(client_id:client_secret)`) |
| 74 | + |
| 75 | +```bash |
| 76 | +curl --request POST \ |
| 77 | + --url https://api.eitri.tech/push-notification-eitri-shop-api/v1/oauth/token \ |
| 78 | + --header 'Content-Type: application/json' \ |
| 79 | + --header 'Authorization: Basic cGtfbGl2ZV8yZjFjLi4uOnMzY3IzdC4uLg==' \ |
| 80 | + --data '{ "grant_type": "client_credentials" }' |
| 81 | +``` |
| 82 | + |
| 83 | +Response: |
| 84 | + |
| 85 | +```json |
| 86 | +{ |
| 87 | + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", |
| 88 | + "token_type": "Bearer", |
| 89 | + "expires_in": 900, |
| 90 | + "scope": "push:send" |
| 91 | +} |
| 92 | +``` |
| 93 | + |
| 94 | +The token is a JWT valid for **900 seconds (15 minutes)** by default — always rely on the returned `expires_in` instead of hardcoding the lifetime. It carries the store (`storeId` / `storeEnvId`) and the credential scopes; you do not need to (and should not) pass the store manually in the following calls. |
| 95 | + |
| 96 | +Reuse the token until it expires instead of requesting a new one for every push. |
| 97 | + |
| 98 | +### Errors |
| 99 | + |
| 100 | +| HTTP | `name` | When it happens | |
| 101 | +| --- | --- | --- | |
| 102 | +| 400 | `unsupported_grant_type` | `grant_type` other than `client_credentials` | |
| 103 | +| 400 | `invalid_request` | Missing `client_id` or `client_secret` | |
| 104 | +| 401 | `invalid_client` | Invalid, unknown or revoked credentials | |
| 105 | + |
| 106 | +--- |
| 107 | + |
| 108 | +## 3. Sending a free-form push |
| 109 | + |
| 110 | +`POST /v1/partner/push` |
| 111 | + |
| 112 | +Requires the `Authorization: Bearer <access_token>` header and the `push:send` scope. |
| 113 | + |
| 114 | +```bash |
| 115 | +curl --request POST \ |
| 116 | + --url https://api.eitri.tech/push-notification-eitri-shop-api/v1/partner/push \ |
| 117 | + --header 'Content-Type: application/json' \ |
| 118 | + --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \ |
| 119 | + --data '{ |
| 120 | + "deviceIds": ["device-1-fcm-token", "device-2-fcm-token"], |
| 121 | + "notification": { |
| 122 | + "title": "Your order is out for delivery", |
| 123 | + "body": "Track it in the app", |
| 124 | + "image": "https://cdn.example.com/banner.png" |
| 125 | + }, |
| 126 | + "data": { |
| 127 | + "deeplink": "eitrishopping://orders/123" |
| 128 | + } |
| 129 | + }' |
| 130 | +``` |
| 131 | + |
| 132 | +### Request body |
| 133 | + |
| 134 | +| Field | Type | Required | Description | |
| 135 | +| --- | --- | --- | --- | |
| 136 | +| `deviceIds` | `string[]` | Yes | Device (FCM) tokens previously registered with Eitri for that store. Must not be empty. | |
| 137 | +| `notification.title` | `string` | Yes | Notification title (must not be empty) | |
| 138 | +| `notification.body` | `string` | Yes | Notification body (must not be empty) | |
| 139 | +| `notification.image` | `string` | No | URL of the image shown in the notification | |
| 140 | +| `data` | `object<string,string>` | No | Extra payload delivered to the app (all values must be strings). Put the target deeplink here. | |
| 141 | +| `apns` | `object` | No | APNs-specific (iOS) overrides forwarded to Firebase | |
| 142 | +| `storeId` / `storeEnvId` | `string` | No | Optional and redundant: if sent, they must match the token exactly, otherwise the request is rejected with `403` | |
| 143 | + |
| 144 | +!!! info "There is no broadcast" |
| 145 | + The API **always** requires an explicit `deviceIds` list. There is no "send to every device in the store" mode — keeping the list of tokens you want to reach is the partner's responsibility. |
| 146 | + |
| 147 | +### Response — `202 Accepted` |
| 148 | + |
| 149 | +```json |
| 150 | +{ |
| 151 | + "accepted": true, |
| 152 | + "requestId": "9c2a5f4e-...", |
| 153 | + "enqueued": 2 |
| 154 | +} |
| 155 | +``` |
| 156 | + |
| 157 | +Delivery is **asynchronous**: the `202` means the message was successfully enqueued, not that the notification already reached the device. Keep the `requestId` — the Eitri team uses it to trace the delivery in the logs. |
| 158 | + |
| 159 | +During processing, each `deviceId` is resolved and validated against the token's store. Devices that are not found or belong to another store are **silently dropped** (and logged for auditing), so `enqueued` reflects what was accepted, not what was actually delivered. |
| 160 | + |
| 161 | +### Errors |
| 162 | + |
| 163 | +| HTTP | `name` | When it happens | |
| 164 | +| --- | --- | --- | |
| 165 | +| 400 | `invalid_target` | `deviceIds` missing, empty, or containing a blank item | |
| 166 | +| 400 | `invalid_notification` | Empty `notification.title` or `notification.body` | |
| 167 | +| 401 | `UNAUTHORIZED` | Missing or invalid token | |
| 168 | +| 401 | `invalid_token` | Revoked or inactive credential | |
| 169 | +| 403 | `FORBIDDEN` | Token without the `push:send` scope, or body `storeId`/`storeEnvId` diverging from the token | |
| 170 | +| 404 | `store.not.found` | Store from the token not found | |
| 171 | + |
| 172 | +--- |
| 173 | + |
| 174 | +## 4. Order status change push |
| 175 | + |
| 176 | +`POST /v1/partner/push/order-status` |
| 177 | + |
| 178 | +An alternative to the previous endpoint for the most common case: telling the customer that the **order status has changed**. Here the partner **does not build the notification nor pick the devices** — it sends only the order identification and the new status, and Eitri does the rest: |
| 179 | + |
| 180 | +1. Receives the `orderId` and the `orderStatus`. |
| 181 | +2. Fetches the order from the store's e-commerce platform (VTEX, Wake or Shopify) to resolve the customer and the order data. |
| 182 | +3. Resolves that customer's devices registered for the store. |
| 183 | +4. Builds the notification from the **message configured for that specific partner and status** and fires the push. |
| 184 | + |
| 185 | +This means notification texts are defined in the integration configuration with Eitri, not per request — to change them, talk to the Eitri team. |
| 186 | + |
| 187 | +Requires the `Authorization: Bearer <access_token>` header, exactly like the free-form push endpoint. |
| 188 | + |
| 189 | +```bash |
| 190 | +curl --request POST \ |
| 191 | + --url https://api.eitri.tech/push-notification-eitri-shop-api/v1/partner/push/order-status \ |
| 192 | + --header 'Content-Type: application/json' \ |
| 193 | + --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \ |
| 194 | + --data '{ |
| 195 | + "orderId": "123", |
| 196 | + "orderStatus": "CHANGE" |
| 197 | + }' |
| 198 | +``` |
| 199 | + |
| 200 | +### Request body |
| 201 | + |
| 202 | +| Field | Type | Required | Description | |
| 203 | +| --- | --- | --- | --- | |
| 204 | +| `orderId` | `string` | Yes | Order identifier in the store's e-commerce platform | |
| 205 | +| `orderStatus` | `string` | Yes | New order status, following the naming used by the store's platform | |
| 206 | + |
| 207 | +The store is always the one in the token — there is no store field in the body, and the same credential cannot look up orders from another store. |
| 208 | + |
| 209 | +### Response — `202 Accepted` |
| 210 | + |
| 211 | +As with the free-form push, processing is asynchronous: the `202` confirms the request was accepted and enqueued, not that the notification was delivered. The response carries a `requestId` for tracing in Eitri's logs. |
| 212 | + |
| 213 | +The push may end up not being sent — with no error returned to the partner — when the order is not found in the platform, the customer has no registered devices, or there is no message configured for that status. Those cases are recorded in Eitri's logs and can be looked up by `requestId`. |
| 214 | + |
| 215 | +### Errors |
| 216 | + |
| 217 | +| HTTP | When it happens | |
| 218 | +| --- | --- | |
| 219 | +| 400 | Missing or empty `orderId` or `orderStatus` | |
| 220 | +| 401 | Missing, invalid or expired token, or revoked credential | |
| 221 | +| 403 | Token without the required scope | |
| 222 | +| 404 | Store from the token not found | |
| 223 | + |
| 224 | +--- |
| 225 | + |
| 226 | +## Best practices |
| 227 | + |
| 228 | +- **Keep the secret in a vault**, never in source code, in a repository or on the front-end. The integration must run from your backend. |
| 229 | +- **Cache the access token** for its lifetime and refresh it only when it expires (or upon a `401`). |
| 230 | +- **Treat `202` as acceptance, not delivery.** If you need delivery confirmation, align with the Eitri team. |
| 231 | +- **Send `deviceIds` in batches** instead of one request per device. |
| 232 | +- **Ask for an immediate rotation** if you suspect the secret has leaked. |
| 233 | + |
| 234 | +## Deeplinks |
| 235 | + |
| 236 | +To route the user to a specific screen when the notification is tapped, use the `data` field with the corresponding deeplink. See [our deeplinks documentation](deeplinks.md) to learn how to structure them. |
0 commit comments