Introduction
Welcome to the GoGift API! The tabs on the right are in two categories:
- Code examples (C#, PHP, Java, etc.)
- Endpoint examples with responses - cURL.
For navigation purposes, refer to the sidebar to the left. Want to look for something in particular? Use the search bar on top!
Want to try out the API in Postman? Click the button below:
Overview
This is an HTTPS-only API, with OpenID authentication and CORS support. We have separate URLs for our Sandbox and Production envirionments. The URLs are as follows:
It uses JSON for all requests, responses, and error messages.
The paragraphs below refer to a general overview of the API, such as authentication, content types of the API, and some common response types.
Breaking Changes
While breaking changes are rarely expected, in the case one is released, you will receive a notification from us describing all affected endpoints and the way the requests should be altered to accomodate the new changes.
Catalogue changes
We do not recommend hardcoding any products, skus, or prices. If you do please be aware that our catalogue is subject to changes. New products are added and existing ones can be discontinued. Similarly certain products are subject to price changes e.g. seasonal gift cards, tickets, experiences and micro gifts. These type of products are usually adjusted at least annually.
Authentication
In order to integrate the purchasing flow provided by the GoGift platform one needs to first obtain a set of credentials (client id and client secret).
The authentication is based on OpenID and once it is successful the client receives a JSON Web Token (JWT) that needs to be used in the rest of the API requests.
The token has an expiration period so it cannot be used indefinitely.
For a code example on how to authenticate, refer to Authentication example.
What is OpenID?
OpenID is an open standard that allows users to log in to multiple websites or applications using a single set of credentials. It provides a decentralized authentication mechanism, allowing users to authenticate themselves without revealing their passwords to each website or application.
How does OpenID work?
-
User Authentication Request: When a user attempts to log in to a website or application using OpenID, the website/application sends an authentication request to the OpenID provider.
-
Authentication by OpenID Provider: The OpenID provider authenticates the user using their credentials (e.g., username and password) or other authentication methods.
-
Authorization Response: Upon successful authentication, the OpenID provider generates an authorization response containing a unique identifier (OpenID token) for the user.
-
User Access Granted: The website/application receives the authorization response from the OpenID provider and grants access to the user based on the provided OpenID token.
What is OAuth?
OAuth (Open Authorization) is an open standard for access delegation, commonly used for secure authorization between applications or services. It allows users to grant limited access to their resources (e.g., data, APIs) to third-party applications without sharing their credentials.
How does OAuth work?
-
Authorization Request: A third-party application requests authorization to access a user’s resources (e.g., user data or APIs) from an OAuth provider.
-
User Authentication and Consent: The OAuth provider authenticates the user and prompts them to grant permission to the third-party application to access their resources.
-
Access Token Generation: Upon user consent, the OAuth provider generates an access token, which represents the authorization granted by the user.
-
Accessing Protected Resources: The third-party application presents the access token to the OAuth provider when accessing the user’s resources (e.g., making API requests).
-
Access Token Validation: The OAuth provider validates the access token and verifies whether the third-party application has the necessary permissions to access the requested resources.
-
Resource Access Granted: If the access token is valid and authorized, the OAuth provider grants access to the user’s resources, allowing the third-party application to retrieve or manipulate the data as per the user’s authorization.
Implementing OpenID and OAuth in Your Code
-
Choose an OpenID Provider: Select an OpenID provider that supports the OpenID Connect protocol for user authentication and authorization.
-
Configure OpenID/OAuth Integration: Configure your application to integrate with the chosen OpenID provider by configuring client credentials, redirect URIs, and scopes.
-
Implement Authentication Flow: Implement the authentication flow in your application, including redirecting users to the OpenID provider for authentication and handling the authorization response.
-
Securely Handle Access Tokens: Ensure that access tokens are securely stored and transmitted in your application, following best practices for token management and security.
-
Access Protected Resources: Use the obtained access tokens to access protected resources, such as APIs or user data, by including the tokens in API requests.
-
Handle Token Expiry and Refresh: Implement mechanisms to handle token expiry and refresh, ensuring seamless authentication and access to resources for users.
If you need additional help and ideas as to how this can be achieved, please take a look at our authentication examples in the section Code examples
Content Types
Below you will find information for the data types of the requests and responses.
Requests
The API expects all writing requests (POST
, PUT
, PATCH
) in JSON format with the Content-Type: application/json
header.
Responses
The API uses JSON as the response format, which is accompanied by the Content-Type: application/json
header in all responses.
Idempotency
Request:
// POST /baskets/finalize HTTP/1.1
// Authorization: Bearer jwt
// Content-Type: application/json
// Idempotency-Key: <idempotency-key>
{
"basketId": "String",
"paymentMethod": "String",
"countryCode": "String",
"redirectConfig": {
"okUrl": "String",
"failUrl": "String",
"cancelUrl": "String"
}
}
Idempotency errors:
- Conflict between idempotency key and request body:
{
"totalTaxAmount": 0,
"totalPaymentAmountWithDiscountNoTax": 0,
"totalPurchaseAmountWithTax": 0,
"totalPaymentAmountWithTax": 0,
"deliveries": null,
"payments": null,
"paymentCurrency": null,
"buyer": null,
"userIdMakingTheBasket": null,
"salesChannelId": null,
"lastUpdatedAt": null,
"createdAt": null,
"publicOrderId": null,
"purchasePhase": null,
"id": null,
"reference": null,
"responseStatus": {
"errorCode": "ArgumentException",
"message": "There is a mismatch between what we have encountered before as a request body and what is being sent for idempotency key \"<key>\". Please ensure that the provided request body does not change for a request with an already provided idempotency key.",
"stackTrace": null,
"errors": [],
"meta": null
}
}
GoGift’s API supports idempotency on select endpoints (See Updating a basket and Finalizing a basket) for safely retrying requests without accidentally performing the same operation twice. When updating or finalizing baskets, use the Idempotency-Key
header (see the example on the right). Then, if a connection error occurs, you can safely repeat the request without risk of creating a second object or performing the update twice.
Common Types
Below you can find some additional information regarding some of the common types you will encounter in the requests and responses of the GoGift API.
Time
All dates are displayed and expected to be in ISO 8601 format in the UTC timezone:
YYYY-MM-DDThh:mm:ssZ
Block | Meaning | Example |
---|---|---|
YYYY |
year | 2023 |
MM |
month number | 09 |
DD |
day of the month | 21 |
T |
date/time separator | |
hh |
hour of the day, in 24-hour format | 14 |
mm |
minutes | 00 |
ss |
seconds | 00 |
Z |
designator for the zero UTC offset |
Localization
Suppose a product’s title has 2 localizations - English and Danish. The
title
field will be sent in the following format:
{
"title": {
"en": "Title in English",
"da": "Titel på dansk",
"sv": null,
"no": null,
"fi": null,
"de": null,
"ru": null,
"fr": null,
"nl": null,
"it": null,
"es": null,
"pl": null,
"cs": null,
"ro": null,
"hu": null,
"bg": null,
"pt": null,
"ja": null,
"el": null,
"tr": null,
"sk": null,
"sl": null
}
}
Fields, such as a product’s title and description, can be localized in several languages. We provide this information on a language-per-language basis. The language codes are as specified by the ISO 639 standard. If a product does not have a localization for a specific language, the value for that language will be set to null
. For a list of all currently supported languages, please refer to the list below. For an example, refer to the tab on the right.
Supported Languages
The list of supported languages will likely expand over time. These changes will be refelcted in the list below.
As of September 21st, 2023, the GoGift platform handles localization for the following languages:
- English
- Danish
- Swedish
- Norwegian
- Finnish
- German
- Russian
- French
- Dutch
- Italian
- Spanish
- Polish
- Czech
- Romanian
- Hungarian
- Bulgarian
- Portugese
- Japanese
- Greek
- Turkish
- Slovakian
- Slovenian
- Croatian
- Estonian
- Irish
- Hebrew
- Latvian
- Lithuanian
- Luxembourgish
- Icelandic
- Hindi
Countries
Countries in the GoGift API are served and handled following the ISO 3166 standard. By default, and unless explicitly specified, countries are represented using a two-letter (alpha-2) code. A full table of the countries with their codes can be found here.
Currencies
Currencies in the GoGift API are served and handled following the ISO 4217 standard. A full table of the currencies with their codes can be found here.
Pagination
Take this example - the response contains 500 items. If you do not provide any paging arguments, you can expect to see the following JSON in the response:
{
"pagingInfo": {
"totalItems": 500,
"totalPages": 10,
"perPage": 50,
"page": 1
}
}
If a request can accept the paging
parameter and/or the response has the pagingInfo
parameter, it means that the response will be paginated. The default value for items returned in a single page is 50
. In the response, alongside the response itself, you will also receive additional information about the paginated response (refer to the table below), such as how many items are in total and over how many pages the response is spread (Refer to the example of the right). By default, the parameter is not required, but if you wish to make use of it, refer to the following table to set the parameters accordingly:
Paging
Parameter | Type | Required | Description | Notes |
---|---|---|---|---|
perPage | integer | ❌ | The number of products to be given per page | Default value: 50 |
page | integer | ❌ | The page to be returned | Default value: 1 |
PagingInfo
Parameter | Type | Description | Notes |
---|---|---|---|
totalItems | integer | The total number of items in the response | |
totalPages | integer | The total number of pages of the response | |
perPage | integer | How many items are present per page | |
page | integer | Which page of the response is currently being returned |
Common Request/Response Types
The following paragraph serves to describe the fields returned by some common request and/or response types, such as baskets.
Example basket response:
{
"totalTaxAmount": 0,
"totalPaymentAmountWithDiscountNoTax": 0,
"totalPurchaseAmountWithTax": 0,
"totalPaymentAmountWithTax": 0,
"deliveries": [
{
"id": "String",
"deliveryDate": "0001-01-01T00:00:00.0000000",
"deliveryMethod": "String",
"recipientName": "String",
"recipientAddress": {
"countryCode": "String",
"city": "String",
"postCode": "String",
"line1": "String",
"line2": "String",
"attention": "String"
},
"recipientEmail": "String",
"recipientPhone": "String",
"recipientWebhookUrl": "String",
"productLines": [
{
"discountPercentage": 0,
"discountTitles": {
"en": "String",
"da": "String",
"sv": "String",
"no": "String",
"fi": "String",
"de": "String",
"ru": "String",
"fr": "String",
"nl": "String",
"it": "String",
"es": "String",
"pl": "String",
"cs": "String",
"ro": "String",
"hu": "String",
"bg": "String",
"pt": "String",
"ja": "String",
"el": "String",
"tr": "String",
"sk": "String",
"sl": "String"
},
"totalPaymentDiscountAmount": 0,
"totalPaymentTaxAmount": 0,
"totalPaymentAmountWithDiscountNoTax": 0,
"paymentDiscountAmount": 0,
"paymentTaxAmount": 0,
"paymentAmountWithDiscountNoTax": 0,
"exchangeRate": 0,
"totalPriceDiscountAmount": 0,
"totalPriceTaxAmount": 0,
"totalPriceWithDiscountNoTax": 0,
"priceDiscountAmount": 0,
"priceTaxAmount": 0,
"priceWithDiscountNoTax": 0,
"giftcardValue": 0,
"valueCurrency": "String",
"quantity": 0,
"productId": "String",
"stockKeepingUnit": "String",
"lineId": "String",
"taxType": "String",
"taxRateMultiplier": 0,
"reference": "String"
}
],
"fees": [
{
"discountPercentage": 0,
"discountTitles": {
"en": "String",
"da": "String",
"sv": "String",
"no": "String",
"fi": "String",
"de": "String",
"ru": "String",
"fr": "String",
"nl": "String",
"it": "String",
"es": "String",
"pl": "String",
"cs": "String",
"ro": "String",
"hu": "String",
"bg": "String",
"pt": "String",
"ja": "String",
"el": "String",
"tr": "String",
"sk": "String",
"sl": "String"
},
"totalPaymentDiscountAmount": 0,
"totalPaymentTaxAmount": 0,
"totalPaymentAmountWithDiscountNoTax": 0,
"paymentDiscountAmount": 0,
"taxType": "String",
"paymentTaxAmount": 0,
"quantity": 0,
"labels": {
"en": "String",
"da": "String",
"sv": "String",
"no": "String",
"fi": "String",
"de": "String",
"ru": "String",
"fr": "String",
"nl": "String",
"it": "String",
"es": "String",
"pl": "String",
"cs": "String",
"ro": "String",
"hu": "String",
"bg": "String",
"pt": "String",
"ja": "String",
"el": "String",
"tr": "String",
"sk": "String",
"sl": "String"
},
"relatedToProductLine": "String",
"targetId": "String",
"targetType": "String",
"lineId": "String",
"paymentAmountWithDiscountNoTax": 0,
"taxRateMultiplier": 0
}
],
"deliveryReference": "String"
}
],
"payments": [
{
"id": "String",
"paymentMethod": "String",
"voucherId": "String"
}
],
"paymentCurrency": "String",
"buyer": {
"accountId": "String",
"accountType": "String",
"name": "String",
"address": {
"countryCode": "String",
"city": "String",
"postCode": "String",
"line1": "String",
"line2": "String",
"attention": "String"
},
"email": "String",
"phone": "String"
},
"userIdMakingTheBasket": "String",
"salesChannelId": "String",
"lastUpdatedAt": "0001-01-01T00:00:00.0000000",
"createdAt": "0001-01-01T00:00:00.0000000",
"publicOrderId": "String",
"purchasePhase": "String",
"id": "String",
"reference": "String",
"responseStatus": { }
}
When operating with baskets (creating and updating) and the result was successful, you can expect the following response from the endpoint:
Basket
Parameter | Type | Description | Notes |
---|---|---|---|
totalTaxAmount | decimal | The total amount of taxes | |
totalPaymentAmountWithDiscountNoTax | decimal | The total amount of products, fees and discounts excluding tax | |
totalPurchaseAmountWithTax | decimal | The total amount of products in this basket including tax | |
totalPaymentAmountWithTax | decimal | Amount that will be processed when paying with tax | |
deliveries | Delivery[] | The deliveries contained in this basket - this is where products, fees etc. are | See Delivery for more information |
payments | Payment[] | The payments used to pay for this basket | See Payment for more information |
paymentCurrency | string | The ISO 4217 code of the currency in which the payment will be done | |
buyer | Buyer | Who is making this purchase | See Buyer for more information |
userIdMakingTheBasket | string | The ID of the user actually making the basket | |
salesChannelId | string | The ID of the sales channel that this basket belongs to | |
lastUpdatedAt | DateTime | When was this basket last updated | |
createdAt | DateTime | When was this basket created | |
publicOrderId | string | The public ID of the order that will be created if the purchase is completed | This can be used for contacting customer service if there are issues encountered with the order |
purchasePhase | string | Which phase of the purchase flow is this basket in | Possible values: Shopping , Paying , PaymentAuthorized , Completed |
id | string | The unique identifier of the basket | |
reference | string | The department reference string |
Buyer
The Buyer
type contains personal information about the buyer, such as their email, name, address, etc.
Parameter | Type | Description | Notes |
---|---|---|---|
accountId | string | The ID of the account making this purchase - if any | |
accountType | string | Tells you what kind of account the accountId is referencing | |
name | string | The name of the buyer | |
address | Address | Physical address of the buyer | |
string | The email address of the buyer | ||
phone | string | The phone number of the buyer |
Address
The Address
type contains information about an address - address lines, country code, etc.
Parameter | Type | Description | Notes |
---|---|---|---|
countryCode | string | See ISO 3166 country code | See Countries for more information |
city | string | The city | |
postCode | string | The post code / ZIP | |
line1 | string | The 1st line of the address | |
line2 | string | The 2nd line of the address | |
attention | string | The attention |
Delivery
The Delivery
type provides information about the individual deliveries in a basket, such as a recepient’s personal information, product lines associated with the given delivery, etc.
Parameter | Type | Description | Notes |
---|---|---|---|
id | string | The unique identifier of the delivery | |
deliveryDate | DateTime | When to deliver this delivery | |
deliveryMethod | string | How to deliver this delivery | Possible values: Physical , Email , CsvByEmail , Sms , Webhook |
recipientName | string | The name of the recipient | |
recipientAddress | Address | The physical address of the recipient | Used when the delivery method is Physical |
recipientEmail | string | The email address of the recipient | Used when the delivery method is Email or CsvByEmail |
recipientPhone | string | The phone number of the recipient | Used when the delivery method is Sms |
recipientWebhookUrl | string | The webhook url of the recipient | Used when the delivery method is Webhook |
productLines | ProductLine[] | The products being bought | See ProductLine for more information |
fees | Fee[] | All the fees applied to this delivery or the products it contains | See Fee for more information |
deliveryReference | string | Delivery reference string |
ProductLine
The ProductLine
type provides information about a product that is part of a given basket.
Parameter | Type | Description | Notes |
---|---|---|---|
discountPercentage | decimal | The percentage of the line’s value amount that is discounted (before tax) | The value is in hundreds - i.e. 512 means 5.12% |
discountTitles | LocalizedString | The label of the discount (if any) | |
totalPaymentDiscountAmount | decimal | Calculated using Quantity * PaymentDiscountAmount |
|
totalPaymentTaxAmount | decimal | Calculated using quantity * paymentTaxAmount |
|
totalPaymentAmountWithDiscountNoTax | decimal | Calculated using quantity * paymentAmountNoTax |
|
paymentDiscountAmount | decimal | priceDiscountAmount converted to payment currency (per unit) |
|
paymentTaxAmount | decimal | priceTaxAmount converted to payment currency (per unit) |
|
paymentAmountWithDiscountNoTax | decimal | (priceNoTax - priceDiscountAmount ) converted to payment currency (per unit) |
|
exchangeRate | decimal | The exchange rate used to convert from value currency to payment currency | |
totalPriceDiscountAmount | decimal | The total discount taking the quantity into account | |
totalPriceTaxAmount | decimal | The total price of tax taking the quantity into account | |
totalPriceWithDiscountNoTax | decimal | The product’s total price excluding tax with discount applied taking the quantity into account | |
priceDiscountAmount | decimal | The actual absolute amount to be subtracted from priceWithDiscountNoTax (per unit) |
|
priceTaxAmount | decimal | The tax of the product’s price (per unit) | |
priceWithDiscountNoTax | decimal | The product’s price excluding tax (per unit) | |
giftcardValue | decimal | For giftcard products, this is the value that will actually be available on the giftcard | |
valueCurrency | string | The ISO 4217 currency code of the product’s spendable value | |
quantity | integer | The quantity (how many units) of the product being bought | |
productId | string | The ID of the product being bought | |
stockKeepingUnit | string | The specific SKU being bought | |
lineId | string | The unique identifier of this line | |
taxType | string | The tax type that applies to this line | |
taxRateMultiplier | decimal | The effective tax rate multiplier for this line | Can be 0 when no tax is applied |
reference | string | Optional reference string |
Fee
The Fee
type provides information about a fee applied to a specific basket.
Parameter | Type | Description | Notes |
---|---|---|---|
discountPercentage | decimal | The percentage of the line’s amount that is discounted (before tax) | The value is in hundreds - i.e. 512 means 5.12% |
discountTitles | LocalizedString | The label of the discount (if any) | |
totalPaymentDiscountAmount | decimal | Calculated using quantity * paymentDiscountAmount |
|
totalPaymentTaxAmount | decimal | Calculated using quantity * paymentTaxAmount |
|
totalPaymentAmountWithDiscountNoTax | decimal | Calculated using quantity * paymentAmountWithDiscountNoTax |
|
paymentDiscountAmount | decimal | The actual absolute amount that has been subtracted from paymentAmountNoTax (per unit) |
|
paymentTaxAmount | decimal | The tax of the payment amount for a single item of this fee | |
quantity | integer | The quantity of the fee | |
labels | LocalizedString | Descriptive text of for the fee | |
relatedToProductLine | string | The ID of the product line that this fee is related to | |
targetId | string | The unique identifier of the thing that the fee specification behind this fee is targeting | |
targetType | string | The fee type | Possible values: Product , DeliveryMethod , B2BDepartment |
lineId | string | The unique identifier of the line the fee is applied to | |
paymentAmountWithDiscountNoTax | decimal | The payment amount for a single item of this fee excluding tax | The discount has already have been applied to this value |
taxType | string | The tax type that applies to this line | |
taxRateMultiplier | decimal | The effective tax rate multiplier for this line | Can be 0 when no tax is present |
Payment
The Payment
type provides information regarding the payment related to the basket once it has been finalized, such as the payment method used.
Parameter | Type | Description | Notes |
---|---|---|---|
id | string | The unique identifier of the payment | |
paymentMethod | string | The payment method of this payment | |
voucherId | string | The ID of the voucher being redeemed (if applicable) |
REST API reference
Below you will find a reference to available endpoints. The format of the documentation is:
- Description of the method used and the endpoint
- Basic information about the purpose of the endpoint
- Request definition with field types, descriptions and notes
- Response definition with field types, descriptions and notes
- Example request and response on the right
Filtering products
Request
// POST | GET /products/filter HTTP/1.1
// Authorization: Bearer jwt
// Content-Type: application/json
{
"salesChannel": "String",
"withDeliveryMethod": "String",
"redeemableInCoutries": "String",
"paging": {
"perPage": 50,
"page": 1
}
}
Response
// HTTP/1.1 200 OK
{
"products": [
{
"availableDeliveryMethods": [
"String"
],
"id": "String",
"redeemableInCountries": [
"String"
],
"title": {
"en": "String",
"da": "String",
// .
// .
// .
// Other languages
},
"shortDescription": {
"en": "String",
"da": "String",
// .
// .
// .
// Other languages
}
}
],
"pagingInfo": {
"totalItems": 0,
"totalPages": 0,
"perPage": 0,
"page": 0
},
"responseStatus": { }
}
POST | GET /products/filter
This endpoint gives basic information about what products can be ordered. In order to retrieve information about available prices refer to Product by ID.
Filtering Products Request
Parameter | Type | Required | Description | Notes |
---|---|---|---|---|
salesChannel | string | ✅ | The sales channel id in which the product is available | A sales channel is an entity that represents the origin of an order and where a product is allowed to be purchased from. We recommend that sales channel with ID 109 is used when filtering products and orders. Of course we can create a custom sales channel in order to provide a limited set of products. |
withDeliveryMethod | string | ❌ | Filter out products that have the provided input as a delivery method | Allowed values: Email , Sms , CsvByEmail , Webhook , Physical |
redeemableInCoutries | string | ❌ | List of codes of countries where the product must be redeemable in | See Countries for more information |
paging | Paging | ❌ | Options regarding pagination | See Pagination for more information |
Filtering Products Response
200 OK
Returns a paginated response containing a list of the products that match the filtering criteria.
Parameter | Type | Description | Notes |
---|---|---|---|
products | Product[] | A list of the products that match the filtering criteria | See Product for more information |
pagingInfo | Paging | Information about the response’s pagination | See Pagination for more information |
Product
Parameter | Type | Description | Notes |
---|---|---|---|
id | string | The unique identifier of the product | |
availableDeliveryMethods | string[] | List of delivery methods | Possible values: Email , Sms , CsvByEmail , Webhook , Physical |
redeemableInCountries | string[] | List of ISO 3166 Alpha-2 codes of countries where the product is redeemable in | |
title | LocalizedString | The title of the product | See Localization for more information |
shortDescription | LocalizedString | A short description of the product | See Localization for more information |
Product by ID
Request
// GET /products/{id} HTTP/1.1
// Authorization: Bearer jwt
// Content-Type: application/json
Response
// HTTP/1.1 200 OK
{
"deliveryMethods": [
{
"deliveryMethod": "String",
"inventory": {
"id": "String",
"inventoryEntries": [
{
"sku": "String",
"priceCurrency": "String",
"salesPriceNoTax": {
"eur": 0,
"usd": 0,
// .
// .
// .
// Other currencies
},
"salesPriceMinNoTax": null,
"salesPriceStepNoTax": null,
"salesPriceMaxNoTax": null
}
]
}
}
],
"id": "String",
"redeemableInCountries": ["String"],
"title": {
"en": "String",
"da": "String",
// .
// .
// .
// Other languages
},
"shortDescription": {
"en": "String",
"da": "String",
// .
// .
// .
// Other languages
},
"responseStatus": {}
}
GET /products/{id}
The following endpoint provides information for a specific product alongside mroe detailed information for its different delivery methods (prices, stock keeping units, etc.).
Product by ID Request
Parameter | Type | Required | Description | Notes |
---|---|---|---|---|
id | string | ✅ | The unique identifier of the product |
Product by ID Response
Parameter | Type | Description | Notes |
---|---|---|---|
deliveryMethods | DeliveryMethod[] | List of delivery methods | |
redeemableInCountries | string[] | List of codes of countries where the product is redeemable in | See Countries for more information |
title | LocalizedString | The title of the product | See Localization for more information |
shortDescription | LocalizedString | A short description of the product | See Localization for more information |
DeliveryMethod
Parameter | Type | Description | Notes |
---|---|---|---|
deliveryMethod | string | The delivery method associated with the product | Possible values: Email , Sms , CsvByEmail , Webhook , Physical |
inventory | Inventory | The inventory associated with the delivery method | See Inventory for more information |
Inventory
Parameter | Type | Description | Notes |
---|---|---|---|
id | string | The unique identifier of the inventory | |
inventoryEntries | InventoryEntry[] | The list of inventory entries associated with the inventory | See InventoryEntry for more information |
InventoryEntry
Parameter | Type | Description | Notes |
---|---|---|---|
sku | string | The stock keeping unit of the inventory entry | See the first notice below for a definition of what a stock keeping unit is |
priceCurrency | string | The default price currency of the inventory entry | |
salesPriceNoTax | Currency | The price of the entry if it is a fixed-price product | null if the product is ranged-price. See the second notice below for a clarification on the different product pricing types |
salesPriceMinNoTax | Currency | The minimum price of the entry if it is a ranged-price product | null if the product is fixed-price. See the second notice below for a clarification on the different product pricing types |
salesPriceMaxNoTax | Currency | The maximum price of the entry if it is a ranged-price product | null if the product is fixed-price. See the second notice below for a clarification on the different product pricing types |
salesPriceStepNoTax | Currency | The step that a ranged-price product entry should increment the prices in | null if the product is fixed-price. See the second notice below for a clarification on the different product pricing types |
Creating a basket
Request
// POST /baskets HTTP/1.1
// Authorization: Bearer jwt
// Content-Type: application/json
{
"salesChannelId": "String"
}
Response
// HTTP/1.1 200 OK
{
"totalTaxAmount": 0,
"totalPaymentAmountWithDiscountNoTax": 0,
"totalPurchaseAmountWithTax": 0,
"totalPaymentAmountWithTax": 0,
"deliveries": [
{
"id": "String",
"deliveryDate": "0001-01-01T00:00:00.0000000",
"deliveryMethod": "String",
"recipientName": "String",
"recipientAddress": {
"countryCode": "String",
"city": "String",
"postCode": "String",
"line1": "String",
"line2": "String",
"attention": "String"
},
"recipientEmail": "String",
"recipientPhone": "String",
"recipientWebhookUrl": "String",
"productLines": [
{
"discountPercentage": 0,
"discountTitles": {
"en": "String",
"da": "String"
// .
// .
// .
// Other languages
},
"totalPaymentDiscountAmount": 0,
"totalPaymentTaxAmount": 0,
"totalPaymentAmountWithDiscountNoTax": 0,
"paymentDiscountAmount": 0,
"paymentTaxAmount": 0,
"paymentAmountWithDiscountNoTax": 0,
"exchangeRate": 0,
"totalPriceDiscountAmount": 0,
"totalPriceTaxAmount": 0,
"totalPriceWithDiscountNoTax": 0,
"priceDiscountAmount": 0,
"priceTaxAmount": 0,
"priceWithDiscountNoTax": 0,
"giftcardValue": 0,
"valueCurrency": "String",
"quantity": 0,
"productId": "String",
"stockKeepingUnit": "String",
"lineId": "String",
"taxType": "String",
"taxRateMultiplier": 0,
"reference": "String"
}
],
"fees": [
{
"discountPercentage": 0,
"discountTitles": {
"en": "String",
"da": "String"
// .
// .
// .
// Other languages
},
"totalPaymentDiscountAmount": 0,
"totalPaymentTaxAmount": 0,
"totalPaymentAmountWithDiscountNoTax": 0,
"paymentDiscountAmount": 0,
"taxType": "String",
"paymentTaxAmount": 0,
"quantity": 0,
"labels": {
"en": "String",
"da": "String"
// .
// .
// .
// Other languages
},
"relatedToProductLine": "String",
"targetId": "String",
"targetType": "String",
"lineId": "String",
"paymentAmountWithDiscountNoTax": 0,
"taxRateMultiplier": 0
}
],
"deliveryReference": "String"
}
],
"payments": [
{
"id": "String",
"paymentMethod": "String",
"voucherId": "String"
}
],
"paymentCurrency": "String",
"buyer": {
"accountId": "String",
"accountType": "String",
"name": "String",
"address": {
"countryCode": "String",
"city": "String",
"postCode": "String",
"line1": "String",
"line2": "String",
"attention": "String"
},
"email": "String",
"phone": "String"
},
"userIdMakingTheBasket": "String",
"salesChannelId": "String",
"lastUpdatedAt": "0001-01-01T00:00:00.0000000",
"createdAt": "0001-01-01T00:00:00.0000000",
"publicOrderId": "String",
"purchasePhase": "String",
"id": "String",
"reference": "String",
"responseStatus": {}
}
POST /baskets
This request creates an empty basket that is ready for use. In order to add/remove products or update basket information see updating a basket.
Create Basket Request
Parameter | Type | Required | Description | Notes |
---|---|---|---|---|
salesChannelId | string | ✅ | The sales channel to which the basket should be associated | We recommend that sales channel 109 is used |
Create Basket Response
200 OK
Returns an empty basket.
Updating a basket
Request
// PUT /baskets HTTP/1.1
// Authorization: Bearer jwt
// Content-Type: application/json
{
"id": "String",
"buyer": {
"accountId": "String",
"accountType": "String",
"name": "String",
"address": {
"countryCode": "String",
"city": "String",
"postCode": "String",
"line1": "String",
"line2": "String",
"attention": "String"
},
"email": "String",
"phone": "String"
},
"paymentCurrency": "String",
"doClearProducts": false,
"addProducts": [
{
"deliveryDate": "0001-01-01T00:00:00.0000000",
"deliveryMethod": "String",
"productReference": "String",
"recipientName": "String",
"recipientAddress": {
"countryCode": "String",
"city": "String",
"postCode": "String",
"line1": "String",
"line2": "String",
"attention": "String"
},
"recipientEmail": "String",
"recipientPhone": "String",
"recipientWebhookUrl": "String",
"stockKeepingUnit": "String",
"productId": "String",
"quantity": 0,
"valueCurrency": "String",
"giftcardValue": 0,
"reference": "String"
}
],
"reference": "String"
}
Response
// HTTP/1.1 200 OK
{
"totalTaxAmount": 0,
"totalPaymentAmountWithDiscountNoTax": 0,
"totalPurchaseAmountWithTax": 0,
"totalPaymentAmountWithTax": 0,
"deliveries": [
{
"id": "String",
"deliveryDate": "0001-01-01T00:00:00.0000000",
"deliveryMethod": "String",
"recipientName": "String",
"recipientAddress": {
"countryCode": "String",
"city": "String",
"postCode": "String",
"line1": "String",
"line2": "String",
"attention": "String"
},
"recipientEmail": "String",
"recipientPhone": "String",
"recipientWebhookUrl": "String",
"productLines": [
{
"discountPercentage": 0,
"discountTitles": {
"en": "String",
"da": "String"
// .
// .
// .
// Other languages
},
"totalPaymentDiscountAmount": 0,
"totalPaymentTaxAmount": 0,
"totalPaymentAmountWithDiscountNoTax": 0,
"paymentDiscountAmount": 0,
"paymentTaxAmount": 0,
"paymentAmountWithDiscountNoTax": 0,
"exchangeRate": 0,
"totalPriceDiscountAmount": 0,
"totalPriceTaxAmount": 0,
"totalPriceWithDiscountNoTax": 0,
"priceDiscountAmount": 0,
"priceTaxAmount": 0,
"priceWithDiscountNoTax": 0,
"giftcardValue": 0,
"valueCurrency": "String",
"quantity": 0,
"productId": "String",
"stockKeepingUnit": "String",
"lineId": "String",
"taxType": "String",
"taxRateMultiplier": 0,
"reference": "String"
}
],
"fees": [
{
"discountPercentage": 0,
"discountTitles": {
"en": "String",
"da": "String"
// .
// .
// .
// Other languages
},
"totalPaymentDiscountAmount": 0,
"totalPaymentTaxAmount": 0,
"totalPaymentAmountWithDiscountNoTax": 0,
"paymentDiscountAmount": 0,
"taxType": "String",
"paymentTaxAmount": 0,
"quantity": 0,
"labels": {
"en": "String",
"da": "String"
// .
// .
// .
// Other languages
},
"relatedToProductLine": "String",
"targetId": "String",
"targetType": "String",
"lineId": "String",
"paymentAmountWithDiscountNoTax": 0,
"taxRateMultiplier": 0
}
],
"deliveryReference": "String"
}
],
"payments": [
{
"id": "String",
"paymentMethod": "String",
"voucherId": "String"
}
],
"paymentCurrency": "String",
"buyer": {
"accountId": "String",
"accountType": "String",
"name": "String",
"address": {
"countryCode": "String",
"city": "String",
"postCode": "String",
"line1": "String",
"line2": "String",
"attention": "String"
},
"email": "String",
"phone": "String"
},
"userIdMakingTheBasket": "String",
"salesChannelId": "String",
"lastUpdatedAt": "0001-01-01T00:00:00.0000000",
"createdAt": "0001-01-01T00:00:00.0000000",
"publicOrderId": "String",
"purchasePhase": "String",
"id": "String",
"reference": "String",
"responseStatus": {}
}
PUT /baskets
This endpoint allows the client to update existing basket information before the basket is finalized. For information on how to finalize an existing basket see Finalizing a basket.
Update Basket Request
Parameter | Type | Required | Description | Notes |
---|---|---|---|---|
id | string | ✅ | The unique identifier of the basket | |
buyer | BuyerInput | ❌ | Information about the buyer | If not set, it will pick up the existing value. See BuyerInput for more information |
paymentCurrency | string | ❌ | The currency code in which the payment will be performed | See Currencies for more information |
doClearProducts | boolean | ❌ | Whether to clear the existing product information on the basket before applying the products in addProducts |
Default value: false . If set to true , all products will be removed from the basket before the new ones are added. |
addProducts | ProductInput[] | ❌ | Products to be added | See ProductInput for more information |
reference | string | ❌ | Text reference to be associated with the basket | Requirement is based on the account config. |
BuyerInput
Parameter | Type | Required | Description | Notes |
---|---|---|---|---|
accountId | string | ❌ | The ID of the account making this purchase | Required when paying via invoice. |
accountType | string | ❌ | Tells you what kind of account the accountId is referencing | Required when paying via invoice. Expected value is B2BDepartment |
name | string | ✅ | The name of the buyer | Required for payment purposes |
address | Address | ✅ | Physical address of the buyer | Required for payment purposes. See Address for more information |
string | ✅ | The email address of the buyer | Required for payment purposes | |
phone | string | ✅ | The phone number of the buyer | Required for payment purposes. The phone number must always start with a country prefix (ex. +45 for Denmark) |
ProductInput
Parameter | Type | Required | Description | Notes |
---|---|---|---|---|
deliveryDate | Date | ❌ | When to deliver this product | If no value is provided, it will assume “as soon as possible” as a delivery date |
deliveryMethod | string | ✅ | How to deliver this product | Allowed values: Email , Sms , CsvByEmail , Webhook , Physical |
productReference | string | ❌ | The product line reference | |
recipientName | string | ✅ | The name of the recipient | |
recipientAddress | Address | ❌ | The address of the recipient | Required if the delivery is Physical . See Address for more information |
recipientEmail | string | ❌ | The email address of the recipient | Required if the delivery is Email or CsvByEmail |
recipientPhone | string | ❌ | The phone number of the recipient | Required if the delivery is Sms . The phone number must always start with a country prefix (ex. +45 for Denmark) |
recipientWebhookUrl | string | ❌ | The webhook url of the recipient | Required if the delivery is Webhook |
stockKeepingUnit | string | ✅ | The exact stock keeping unit being bought | Obtained from Product by ID |
productId | string | ✅ | The ID of the product being added | |
quantity | integer | ✅ | How many of the product to get | |
valueCurrency | string | ✅ | The currency code of product’s value | See Currencies for more information |
giftcardValue | decimal | ✅ | The value the giftcard should have |
Update Basket Response
200 OK
Returns a basket with the new products that were added after the update.
Finalizing a basket
Request
// POST /baskets/finalize HTTP/1.1
// Authorization: Bearer jwt
// Content-Type: application/json
{
"basketId": "String",
"paymentMethod": "String",
"countryCode": "String",
"redirectConfig": {
"okUrl": "String",
"failUrl": "String",
"cancelUrl": "String"
}
}
Response
// HTTP/1.1 200 OK
{
"nextStep": "String",
"paymentWindowUrl": "String",
"orderId": "String",
"publicOrderId": "String",
"responseStatus": {}
}
Finalize Basket Request
Parameter | Type | Required | Description | Notes |
---|---|---|---|---|
basketId | string | ✅ | The ID of the basket to finalize | |
paymentMethod | string | ✅ | What payment method should be used | Allowed values: InvoiceByFinance , ExternalPsp , Trustly |
countryCode | string | ❌ | The country / region the user is shopping in | Value type: ISO 3166 country code |
redirectConfig | RedirectConfig | ❌ | URLs to use for completed, failed and canceled payments in the payment window | Required if the payment type is ExternalPsp or Trustly . See RedirectConfig for more information |
RedirectConfig
Parameter | Type | Required | Description | Notes |
---|---|---|---|---|
okUrl | string | ✅ | The URL the user should be redirected to when the payment has gone through successfully | |
failUrl | string | ✅ | The URL the user should be redirected to when there is an error during the payment process (insufficient funds, etc.) | |
cancelUrl | string | ✅ | The URL the user should be redirected to when the payment has been cancelled by the user |
Finalize Basket Response
200 OK
Returns information about the next steps that should be taken, alongside metadata about the order that is to be placed once payment is complete:
Parameter | Type | Description | Notes |
---|---|---|---|
nextStep | string | What the system should do next | Example: If the value is ShowConfirmationPage , you are expected to redirect to an info page informing the user of the order status (completed, canceled, etc.) |
paymentWindowUrl | string | The URL to which to send the user | Applicable if the payment is ExternalPsp or Trustly |
orderId | string | The ID of the order generated | Only set if the payment has already been fulfilled |
publicOrderId | string | The public ID of the order generated | Only set if the payment has already been fulfilled |
Information about Webhook delivery fulfillment
Request reference
// POST https://your_webhook_url
{
"webhookId": "String",
"timeStamp": "0001-01-01T00:00:00.0000000",
"content": [
{
"productId": "String",
"orderId": 0,
"deliveryId": 0,
"amountInDecimal": 0,
"currencyCode": "String",
"createdAt": "0001-01-01T00:00:00.0000000",
"expiresAt": "0001-01-01T00:00:00.0000000",
"giftcardUri": "String",
"pin": "String"
}
],
"message": "String"
}
In order to receive a fulfilled order you need to provide a valid publicly accessible URL in the basket (see Updating a basket). That endpoint should be able to receive POST
requests, where the body of the request is defined by GoGift and is sent in a JSON format.
Webhook body
Parameter | Type | Description | Notes |
---|---|---|---|
webhookId | string | The unique identifier of the webhook | |
timeStamp | DateTime | When was the webhook request dispatched | See Time for more information |
content | WebhookContent[] | See WebhookContent for more information | |
message | string | An informative message regarding the order |
WebhookContent
Parameter | Type | Description | Notes |
---|---|---|---|
productId | string | The ordered product ID | |
orderId | long | The ID of the associated order | |
deliveryId | long | The associated delivery ID | |
amountInDecimal | decimal | The value of the gift card | |
currencyCode | string | The currency code of the gift card | |
createdAt | DateTime | When was the gift card created | See Time for more information |
expiresAt | DateTime | When does the gift card expire | See Time for more information |
giftcardUri | string | The URI to be used for the retrieval of the giftcard | |
pin | string | The PIN to be used for verifying the giftcard retrieval |
Validating the origin of the request
The origin of the request can be validated by verifying the provided signature. An example on how that can be done can be found in Webhook signature validation.
In order to validate the signature a ClientId
and WebhookSecret
must be provided from GoGift.
Code Examples
The sections below serve to demonstrate some example code for different parts of our system, such as authentication, a full integration example, and a way to verify the webhook signature.
Authentication example
using IdentityModel.Client;
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace GoGift.Examples
{
public class AuthExample
{
public async Task<string> GetJwtAsync()
{
var httpClient = new HttpClient();
var discovery = await httpClient.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest
{
Address = "the_auth_url",
Policy = new DiscoveryPolicy
{
RequireHttps = true
}
});
if (discovery.IsError)
{
throw new Exception(discovery.Error);
}
var tokenResponse = await httpClient.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = discovery.TokenEndpoint,
ClientId = "your_client_id",
ClientSecret = "your_client_secret"
});
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
return tokenResponse.AccessToken; // the Jwt that needs to be used for communication with the API
}
}
}
<?php
require __DIR__ . '/vendor/autoload.php';
use Jumbojett\OpenIDConnectClient;
class AuthExample {
private const PROVIDER_DOMAIN = 'the_auth_url';
public function GetJwt() {
$oidc = new OpenIDConnectClient(
self::PROVIDER_DOMAIN,
'your_client_id',
'your_client_secret'
);
$oidc->providerConfigParam(
array('token_endpoint' => self::PROVIDER_DOMAIN.'/connect/token')
);
return $clientCredentialsToken = $oidc->requestClientCredentialsToken()->access_token;
}
}
//
// show the returned access token
//
$autnExample = new AuthExample();
$jwt = $autnExample->GetJwt();
echo $jwt;
?>
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser;
import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
import com.nimbusds.oauth2.sdk.*;
import com.nimbusds.oauth2.sdk.auth.*;
import com.nimbusds.oauth2.sdk.id.*;
public class App
{
public static void main( String[] args ) throws ParseException, IOException, URISyntaxException
{
String domain = "the_auth_url";
ClientAuthentication clientAuth = new ClientSecretBasic(
new ClientID("your_client_id"), new Secret("your_client_secret"));
URI tokenEndpoint = new URI(String.format("%s/connect/token", domain));
TokenRequest request = new TokenRequest(tokenEndpoint, clientAuth, new ClientCredentialsGrant());
TokenResponse response = OIDCTokenResponseParser.parse(request.toHTTPRequest().send());
if (response.indicatesSuccess()) {
OIDCTokenResponse successResponse = (OIDCTokenResponse)response.toSuccessResponse();
OIDCTokens tokens = successResponse.getOIDCTokens();
System.out.println(String.format("Resulting JWT: %s", tokens.getAccessToken()));
}
}
}
const openIdClient = require("openid-client");
const GRANT_TYPE = "client_credentials";
const issuer = openIdClient.Issuer.discover("the_auth_url");
issuer.defaultHttpOptions = { timeout: 3500 };
issuer.then((issuer) => {
const client = new issuer.Client({
client_id: "your_client_id",
client_secret: "your_client_secret",
});
client
.grant({
grant_type: GRANT_TYPE,
})
.then((token) => {
console.info(`Resulting JWT: ${token.access_token}`);
});
});
Make sure to replace
your_client_id
andyour_client_secret
with your respective client ID and client secret.
The code examples on the right serve as a step-by-step guide on how to authenticate against the GoGift API and obtain a JWT token.
The example code in .NET
is based on the IdentityModel library. The version of the library that should be used is 4.6.0
.
The example code in PHP
is based on the OpenId-Connect-PHP library.
The example code in Java
is based on the Nimbus library.
The example code in Node.js
is based on the openid-client library.
Integration example
using IdentityModel.Client;
using ServiceStack;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
namespace GoGift.Example
{
public class Program
{
public static void Main(string[] args)
{
var example = new OrderFlowExample(new HttpClient());
await example.RunEmailDeliveryExampleAsync();
}
}
public class OrderFlowExample
{
private readonly HttpClient _httpClient;
public OrderFlowExample(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task RunEmailDeliveryExampleAsync()
{
var accessToken = await GetJwtAsync();
// get products
var productsResponse = await SendRequestAsync<FilterProductsRequest, ProductsResponse>(
accessToken,
new FilterProductsRequest
{
RedeemableInCoutries = new List<string> { "DK" },
Paging = new Paging
{
Page = 1,
PerPage = 1
}
},
HttpMethod.Post,
"/products/filter");
var product = productsResponse.Products.FirstOrDefault();
if(product != default)
{
var deliveryInfo = await SendRequestAsync<GetProductRequest, ProductResponse>(
accessToken,
new GetProductRequest
{
Id = product.Id
},
HttpMethod.Get,
$"/products/{product.Id}");
var prices = deliveryInfo.DeliveryMethods.FirstOrDefault(x => x.DeliveryMethod == "Email")?.Inventory;
if(prices != default)
{
// create a basket and add the relevant product information
var basket = await SendRequestAsync<CreateBasketRequest, BasketResponse>(
accessToken,
new CreateBasketRequest
{
SalesChannelId = "109"
},
HttpMethod.Post,
"/baskets");
await SendRequestAsync<UpdateBasketRequest, BasketResponse>(
accessToken,
new UpdateBasketRequest
{
Id = basket.Id,
Buyer = new BasketBuyer
{
AccountId = "your_b2b_department_id",
AccountType = "B2BDepartment",
Name = "Valid byer name",
Email = "email@example.com",
Phone = "+4500000",
Address = new Address
{
Line1 = "Valid address",
City = "Valid city",
CountryCode = "DK",
PostCode = "2000"
}
},
AddProducts = new List<AddProduct>
{
new AddProduct
{
DeliveryDate = DateTime.UtcNow,
DeliveryMethod = "Email",
RecipientName = "Recipient Name",
RecipientEmail = "email@example.com",
StockKeepingUnit = prices.InventoryEntries[0].Sku,
ProductId = product.Id,
Quantity = 1,
GiftcardValue = prices.InventoryEntries[0].SalesPriceNoTax.Dkk,
ValueCurrency = "DKK"
}
}
},
HttpMethod.Put,
"/baskets");
// finalize the basket
var order = await SendRequestAsync<FinalizeBasketRequest, FinalizeBasketResponse>(
accessToken,
new FinalizeBasketRequest
{
BasketId = basket.Id,
PaymentMethod = "invoicebyfinance",
CountryCode = "DK"
},
HttpMethod.Post,
"/baskets/finalize");
Console.WriteLine($"Completed order with ID {order.OrderId}");
}
}
}
public async Task<string> GetJwtAsync()
{
var discovery = await _httpClient.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest
{
Address = "the_auth_url",
Policy = new DiscoveryPolicy
{
RequireHttps = false
}
});
if (discovery.IsError)
{
throw new Exception(discovery.Error);
}
var tokenResponse = await _httpClient.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = discovery.TokenEndpoint,
ClientId = "your_client_id",
ClientSecret = "your_client_secret"
});
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
return tokenResponse.AccessToken;
}
public async Task<TResponse> SendRequestAsync<T, TResponse>(
string jwt,
T payload,
HttpMethod method,
string path)
{
var httpResponse = await SendRequestAsync<T>(jwt, payload, method, path);
if (httpResponse?.Content == null)
{
return default(TResponse);
}
var responseString = await httpResponse.Content.ReadAsStringAsync();
return responseString.FromJson<TResponse>();
}
private async Task<HttpResponseMessage> SendRequestAsync<T>(
string jwt,
T payload,
HttpMethod method,
string path)
{
var requestContent = SerializeRequestPayload(payload);
var requestMessage = new HttpRequestMessage(method, $"the_api_url{path}")
{
Method = method,
Content = new StringContent(requestContent, Encoding.UTF8, "application/json")
};
requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
var httpResponse = await _httpClient.SendAsync(requestMessage);
if (!httpResponse.IsSuccessStatusCode)
{
throw new Exception("Unsuccessful request");
}
return httpResponse;
}
private string SerializeRequestPayload<T>(T payload)
{
if (payload == null)
{
return string.Empty;
};
var strPayload = payload as string;
if (strPayload != null)
{
return strPayload;
}
return payload.ToJson();
}
}
}
<?php
require __DIR__ . "/vendor/autoload.php";
use Jumbojett\OpenIDConnectClient;
class OrderFlowExample {
private const PROVIDER_DOMAIN = "the_auth_url";
private const API_DOMAIN = "the_api_url";
private GuzzleHttp\Client $client;
public function __construct() {
$this->client = new GuzzleHttp\Client(["base_uri" => self::API_DOMAIN]);
}
public function RunEmailDeliveryExample() {
$accessToken = $this->GetJwt();
// get products
$filter = $this->SendRequest(
$accessToken,
(object) [
"redeemableInCoutries" => ["DK"],
"paging" => (object) [
"page" => 1,
"perPage" => 1
]
],
"POST",
"/products/filter"
);
if($filter->products[0] != null) {
$deliveryInfo = $this->SendRequest(
$accessToken,
null,
"GET",
"/products/{$filter->products[0]->id}"
);
$key = array_search("Email", array_column($deliveryInfo->deliveryMethods, 'deliveryMethod'));
if($deliveryInfo->deliveryMethods[$key] != null) {
$prices = $deliveryInfo->deliveryMethods[$key]->inventory;
// create a basket and add the relevant product information
$basket = $this->SendRequest(
$accessToken,
(object) [
"salesChannelId" => "109"
],
"POST",
"/baskets"
);
$this->SendRequest(
$accessToken,
(object) [
"id" => $basket->id,
"buyer" => (object) [
"accountId" => "your_b2b_department_id",
"accountType" => "B2BDepartment",
"name" => "Valid byer name",
"email" => "email@example.com",
"phone" => "+4500000",
"address" => (object) [
"line1" => "Valid address",
"city" => "Valid city",
"countryCode" => "DK",
"postCode" => "2000"
]
],
"addProducts" => [
(object) [
"deliveryDate" => gmdate("Y-m-d\TH:i:s\Z"),
"deliveryMethod" => "Email",
"recipientName" => "Recipient Name",
"recipientEmail" => "email@example.com",
"stockKeepingUnit" => $prices->inventoryEntries[0]->sku,
"productId" => $filter->products[0]->id,
"quantity" => 1,
"giftcardValue" => $prices->inventoryEntries[0]->salesPriceNoTax->dkk,
"valueCurrency" => "DKK"
]
]
],
"PUT",
"/baskets"
);
// finalize the basket
$order = $this->SendRequest(
$accessToken,
(object) [
"basketId" => $basket->id,
"paymentMethod" => "invoicebyfinance",
"countryCode" => "DK"
],
"POST",
"/baskets/finalize"
);
echo "Completed order with ID {$order->orderId}";
}
}
}
private function GetJwt() {
$oidc = new OpenIDConnectClient(
self::PROVIDER_DOMAIN,
"your_client_id",
"your_client_secret"
);
$oidc->providerConfigParam(
array("token_endpoint" => self::PROVIDER_DOMAIN."/connect/token")
);
return $clientCredentialsToken = $oidc->requestClientCredentialsToken()->access_token;
}
private function SendRequest(string $jwt, ?\stdClass $payload, string $method, string $path) {
$options = [
"headers" => [
"Content-Type" => "application/json",
"Accept" => "application/json",
"Authorization" => "Bearer {$jwt}"
]
];
if($payload != null) {
$options["json"] = $payload;
}
$response = $this->client->request($method, $path, $options);
return json_decode($response->getBody());
}
}
//
// Run the order example
//
$example = new OrderFlowExample();
$example->RunEmailDeliveryExample();
?>
The current example illustrates a step by step integration of the purchasing flow in its most basic form. The classes that define requests and responses are not included in this example, because they may vary from integration to integration, therefore the example cannot be run just by being copied in a new project as it is meant only for illustrative purposes.
The PHP code example is based on the libraries:
Webhook signature validation
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace bootstrap_example
{
public class WebhookHeaderValidation
{
private const string ClientId = "your_client_id";
private const string WebhookSecret = "your_webhook_secret";
private async Task<bool> RequestIsValid(HttpRequest request)
{
var headers = request.Headers.ToDictionary(h => h.Key, h => h.Value);
if (headers.ContainsKey("Signature"))
{
var signatureValues = ParseSignature(headers["Signature"].FirstOrDefault());
if (signatureValues.ContainsKey("algorithm") && signatureValues["algorithm"] == "hmac-sha256")
{
var date = headers["X-Request-Datetime"].FirstOrDefault();
var clientId = headers["X-Client-ID"].FirstOrDefault();
var requestId = headers["X-Request-ID"].FirstOrDefault();
#region validate headers and signature
var isValid = true;
if (!clientId.Equals(ClientId, StringComparison.InvariantCultureIgnoreCase))
{
isValid = false;
}
// OPTIONAL: Validate that the request has been sent no later than 60 seconds
// or the appropriate amount of time acceptable for the solution
if ((DateTime.UtcNow - Convert.ToDateTime(date)).Seconds > 60)
{
isValid = false;
}
// OPTIONAL: The X-Request-ID can be used to validate that the webhook request
// has not been sent more than once
using (var sha256 = SHA256.Create())
{
var body = await new StreamReader(request.Body).ReadToEndAsync();
var hashedBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(body));
var content = Convert.ToBase64String(hashedBytes);
var reformedToken = $"{request.Method}{date}{clientId}{requestId}{content}";
var authenticationTokenBytes = Encoding.UTF8.GetBytes(reformedToken);
using (var hmac = new HMACSHA256(Convert.FromBase64String(WebhookSecret)))
{
hashedBytes = hmac.ComputeHash(authenticationTokenBytes);
string reformedTokenBase64String = Convert.ToBase64String(hashedBytes);
if (!signatureValues["signature"].Equals(reformedTokenBase64String, StringComparison.InvariantCultureIgnoreCase))
{
isValid = false;
}
}
}
return isValid;
}
}
return false;
}
private Dictionary<string, string> ParseSignature(string signatureHeader)
{
var result = new Dictionary<string, string>();
var parameters = signatureHeader.Split(',');
foreach (var parameter in parameters)
{
var values = parameter.Split('=', 2);
result.Add(values[0], values[1].Replace("\"", ""));
}
return result;
}
}
}
<?php
// Helper methods for the example
function getRequestHeaders() {
$headers = array();
foreach($_SERVER as $key => $value) {
if (substr($key, 0, 5) <> 'HTTP_') {
continue;
}
$header = str_replace(' ', '-', ucwords(str_replace('_', ' ', strtolower(substr($key, 5)))));
$headers[$header] = $value;
}
return $headers;
}
function parseSignature($signatureHeader) {
$result = array();
$parameters = explode(',', $signatureHeader);
foreach($parameters as &$parameter) {
$values = explode('=', $parameter, 2);
$result[$values[0]] = str_replace('"', '', $values[1]);
}
return $result;
}
// Signature verification logic
const Signature = 'Signature';
const Datetime = 'X-Request-Datetime';
const ClientId = 'X-Client-Id';
const RequestId = 'X-Request-Id';
const ClientSecret = 'your_client_secret';
$headers = getRequestHeaders();
if ($headers[Signature]) {
$signatureValues = parseSignature($headers[Signature]);
$date = $headers[Datetime];
$clientId = $headers[ClientId];
$requestId = $headers[RequestId];
$hashedBytes = hash('sha256', file_get_contents('php://input'), true);
$content = base64_encode($hashedBytes);
$reformedToken = $_SERVER['REQUEST_METHOD'].$date.$clientId.$requestId.$content;
$reformedTokenBase64String = base64_encode(hash_hmac(
'sha256',
$reformedToken,
base64_decode(ClientSecret),
true
));
return strcasecmp($reformedTokenBase64String, $signatureValues['signature']) == 0;
}
return false;
?>
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.text.ParseException;
import java.text.SimpleDateFormat;
public class WebhookHeaderValidation {
private static final String CLIENT_ID = "your_client_id";
private static final String WEBHOOK_SECRET = "your_webhook_secret";
// NOTE: Expected value of the method parameter is "POST"
public boolean requestIsValid(String method, String body, Map<String, String> headers) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
if (headers.containsKey("Signature")) {
Map<String, String> signatureValues = parseSignature(headers.get("Signature"));
if (signatureValues.get("algorithm").equals("hmac-sha256")) {
String date = headers.get("X-Request-Datetime");
String clientId = headers.get("X-Client-ID");
String requestId = headers.get("X-Request-ID");
boolean isValid = CLIENT_ID.equalsIgnoreCase(clientId);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
// OPTIONAL: Validate that the request has been sent no later than 60 seconds
// or the appropriate amount of time acceptable for the solution
try {
Date dateObj = sdf.parse(date);
if ((new Date().getTime() - dateObj.getTime()) / 1000 > 60) {
isValid = false;
}
} catch (ParseException e) {
isValid = false;
}
// OPTIONAL: The X-Request-ID can be used to validate that the webhook request
// has not been sent more than once
MessageDigest sha256Digest = MessageDigest.getInstance("SHA-256");
byte[] hashedBytes = sha256Digest.digest(body.getBytes(StandardCharsets.UTF_8));
String content = Base64.getEncoder().encodeToString(hashedBytes);
String reformedToken = method + date + clientId + requestId + content;
byte[] authenticationTokenBytes = reformedToken.getBytes(StandardCharsets.UTF_8);
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(new SecretKeySpec(Base64.getDecoder().decode(WEBHOOK_SECRET), "HmacSHA256"));
hashedBytes = hmac.doFinal(authenticationTokenBytes);
String reformedTokenBase64String = Base64.getEncoder().encodeToString(hashedBytes);
if (!signatureValues.get("signature").equalsIgnoreCase(reformedTokenBase64String)) {
isValid = false;
}
return isValid;
}
}
return false;
}
private Map<String, String> parseSignature(String signatureHeader) {
Map<String, String> result = new HashMap<>();
String[] parameters = signatureHeader.split(",");
for (String parameter : parameters) {
String[] values = parameter.split("=", 2);
result.put(values[0], values[1].replace("\"", ""));
}
return result;
}
}
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.raw({ type: '*/*', limit: '10mb' }));
const port = 9000;
const Signature = 'signature';
const Datetime = 'x-request-datetime';
const ClientId = 'x-client-id';
const RequestId = 'x-request-id';
const ClientSecret = 'your_client_secret';
const parseSignature = (signature) => {
const result = {};
const parameters = signature.split(',');
parameters.forEach(parameter => {
const values = [];
const index = parameter.indexOf('=');
values.push(parameter.substring(0, index));
values.push(parameter.substring(index + 1));
result[values[0]] = values[1].replaceAll('"', '');
});
return result;
}
app.post('/signature', (req, res) => {
let isValid = false;
if (req.headers[Signature]) {
const signatureValues = parseSignature(req.headers[Signature]);
const date = req.headers[Datetime];
const clientId = req.headers[ClientId];
const requestId = req.headers[RequestId];
const hashedBytes = crypto.createHash('sha256').update(req.body.toString('utf-8')).digest();
const content = hashedBytes.toString('base64');
const reformedToken = `${req.method}${date}${clientId}${requestId}${content}`;
const reformedTokenBase64String = crypto.createHmac('sha256', Buffer.from(ClientSecret, 'base64'))
.update(reformedToken).digest().toString('base64');
isValid = reformedTokenBase64String === signatureValues[Signature];
}
res.send(isValid);
})
app.listen(port, () => {
console.log(`Example: Listening on port ${port}`);
})
Example code in .NET, PHP, Java and JavaScript showing how the origin of the incoming webhook request can be validated via the provided request signature.
In order to validate that the webhook request has not been tampered with and that it is coming from the correct origin, you need to validate the Signature
header.
This can be done via constructing a hash via hmac-sha256
algorithm, using the X-Request-Datetime
, X-Client-ID
, X-Request-ID
, the request method and the body of the request base64 encoded.
Additionally you can use the headers to ensure that the given ClientId
is correct and that the date of delivery is correct.
You can find more general information about the provided signature in Section 4.1 of Signing HTTP Messages