# API specs of Markets data sharing

This page describes API specs that Markets need to expose for data sharing. If you do not use the data sharing feature, you can skip this page.

# Overview

The following information needs to be provided by a Market to Credify for Market integration.

  • List of basic profile data
  • Definition of custom data scope

Endpoints:

  • Push claims API endpoint
  • User segment API endpoint
  • Offer evaluation API endpoint
  • Encryption claims API endpoint
  • Offer filtering API endpoint

Of these 5 APIs, only Push claims API and Encryption claims API endpoints are necessary when a Market shares data. The other APIs are optional if you want to deal with customized financial offers for you users. Markets will need to implement these APIs using our SDK, then register their endpoints on our Dashboard. To add the endpoints, please go to Settings -> General information -> Update -> Account details -> update API Base Url

Product without an offer Product with an offer
with MP data Push claim API & Encrypted claim API All 5 integration APIs are necessary
without MP data Only Intent API integration / Retail portal Only Intent API integration / Retail portal

TIP

We have developed a skeleton service to minimize the integration work, which handles most common logic and SDK interaction. We provide the skeleton service in Node.js as of May 2022. All you would have to do is to fork the repository and to customize some database interaction only.

The following sections relating to the backend implementation are not necessary if you use the skeleton service. You can find example implementation of the above 5 APIs in our skeleton.

Integration model


# List of basic profile data

Each Market needs to set what piece of profile information it can provide with Service Providers. Here is a list of data points:

  • phone - e.g., +84381234567
  • email - e.g., test@credify.one
  • address - e.g., 1st Floor, Savimex Building, 194 Đ. Nguyễn Công Trứ, Phường Nguyễn Thái Bình, Quận 1, Thành phố Hồ Chí Minh
  • gender - e.g., male / female
  • dob - e.g., 1991-12-25
  • name - e.g., Nguyển Minh Long

If a Market does not provide any of user data, then it can be empty list.

# Definition of custom data scope

Scope definition is a custom data schema that reflects a user's personal information outside of basic profile data that a Market will share with Financial Institutions. An example of custom data scope would be an end user's monthly transaction. The users need to agree to share these data for the data transmission to go through. By doing so, users can get better financial product as it leverages their data to improve their credibility. Financial Institutions can filter scope definition to find which users fit their target.

This property is optional. You may provide this information to enhance UX to offer financial products for right user segments.

# Push claims API endpoint

This API is used to verify that a Market has data about a specific user. When a user logs in to your platform with their Credify account, you can call this API with their internal ID in your system. The API will then map this ID with the user's unique identifier in the ServiceX system, verifying that the user has consented to sharing their data with your Market.

# API definition

API doc for Push claim API

# Implementation

Here is what we need you to do in this API endpoint

  • Extract the internal user ID and their Credify ID from the API request payload.
  • Save the mapping between the internal user ID and their Credify ID in your database, as well as Credify's scopes with your internal data. This mapping will be crucial for the following steps
  • Compose a claim object with the user's data stored in your database.
  • Push the claim object using our SDK's push claims function. The function will take in your organization ID, the Credify ID from the user, and the claim object.
  • You will receive a commitment object signifying that you have data about the user. Save this object in your database for later use.
  • Return a 200 status code with response body being the Credify ID of the user.

To implement these steps, you need to find the user associated with the internal ID passed from the request, save their Credify ID along with the user, and save the commitments returned by the SDK's function in your database.

A claim token is a JWT whose payload is following.

  • scope_name: string
    • Unique scope name registered in serviceX Dashboard
    • In case of standard claims, this will be predefined, such as profile, phone, email, and address.
  • scope_hash: string
    • sha256 hashed value, like CREDIT_SCORE_SCOPE_HASH / EMAIL_SCOPE_HASH in the above description.
  • timestamp: integer
    • Unix timestamp in sec
  • user_claim_token: string
    • JWT to ensure this data attachment is coming from a Market. Its payload is provider_id, user_id, scope_name, scope_hash, and timestamp, like CREDIT_SCORE_CLAIM_TOKEN_PAYLOAD / EMAIL_CLAIM_TOKEN_PAYLOAD.

Here is a sample.

// In the case of your `custom` claims
const CREDIT_SCORE_SCOPE_DATA = {
  YOUR_CLAIM_A: "CLAIM_A_VALUE",
  YOUR_CLAIM_B: "CLAIM_B_VALUE",
  YOUR_CLAIM_C: "CLAIM_C_VALUE",
  "credit-score:commitment": "SECRET_RANDOM_COMMITMEMT",
};
const CREDIT_SCORE_SCOPE_HASH = sha256(JSON(CREDIT_SCORE_SCOPE_DATA));
const CREDIT_SCORE_CLAIM_TOKEN_PAYLOAD = {
  scope_name: "credit-score",
  user_id: "a6367e50-2d4c-...", // passed from the mobile SDK
  scope_hash: CREDIT_SCORE_SCOPE_HASH,
  timestamp: 1612197298,
};

// In the case of your `standard` claims
const EMAIL_SCOPE_DATA = {
  email: "test@credify.one",
  email_commitment: "SECRET_RANDOM_COMMITMEMT",
};
const EMAIL_SCOPE_HASH = sha256(JSON(CREDIT_SCORE_SCOPE_DATA));
const EMAIL_CLAIM_TOKEN_PAYLOAD = {
  scope_name: "email",
  user_id: "a6367e50-2d4c-...", // passed from the mobile SDK
  scope_hash: EMAIL_SCOPE_HASH,
  timestamp: 1612197298,
};
  • Node.js
  • Java
  • .NET
const pushClaims = async (req, res, { db, credify }) => {

  // This step is optional if you want to authenticate your internal requests
  const validRequest = await authenticateInternalUser(db, req);
  if (!validRequest) {
    return res.status(401).send({ message: "Unauthorized" })
  }

  const organizationId = process.env.APP_ID
  if (!organizationId) {
    return res.status(400).send({ message: "Please recheck config - organization ID" })
  }

  if (!req.body.id || !req.body.credify_id) {
    return res.status(400).send({ message: "Invalid body" })
  }

  try {
    const localId = req.body.id
    const credifyId = req.body.credify_id

    // save the mapping between credify ID and your user's local ID for later use. 
    await updateUserId(db, localId, credifyId);
    // get the claim object from the data mapping between credify and your system
    const claims = await fetchUserClaimObject(db, localId, credifyId, [], false);

    const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
    await delay(3000);

    const commitments = await credify.claims.push(
      organizationId,
      credifyId,
      claims
    )

    await upsertCommitments(db, credifyId, commitments);

    res.json({ credifyId })
  } catch (e) {
    res.status(500).send({ message: e.message })
  }
}

module.exports = pushClaims

# User segment API endpoint

This API is used to retrieve user segments based on specified thresholds. When Service Providers configure offers in Credify dashboard, they can set thresholds for certain criteria (e.g. credit score, income, etc.). The User Segment API can then be used to check how many users in your system fall under each threshold. This can help Service Providers better target their offers to users who meet their criteria.

image

# API definition

API doc for user segment API

# Implementation

What you have to do by yourself is

  • Validate access token attached in the API request authorization header. To validate, use the introspectToken function in our SDK. The scope you need to check is oidc_client:read_user_counts.
  • Extract offer conditions from our API request payload
  • Count the number of users who meet each extracted offer conditions.
  • Return the counts, with each count stands for the index of the corresponding condition.
  • Node.js
  • Java
  • .NET
const countUsers = async (req, res, { db, credify }) => {
  if (process.env.CONTEXT_ENV !== "Jest") {
    const token = extractToken(req)
    try {
      const validToken = await credify.auth.introspectToken(
        token,
        PERMISSION_SCOPE.COUNT_USER
      )
      if (!validToken) {
        return res.status(401).send({ message: "Unauthorized" })
      }
    } catch (e) {
      return res.status(500).send({ message: e.message })
    }
  }

  let conditions = req.body.conditions || [{}]
  conditions = conditions.map((c) => {
    if (c === null) return {}
    else return c
  })
  const requiredCustomScopes = req.body.required_custom_scope || []
  if (conditions.length !== requiredCustomScopes.length) {
    return res.status(400).send({ message: "Conditions length and scopes length must be the same" })
  }

  const claims = fetchUserClaimObject

  // this function queryUsers would be your internal API 
  const counts = await queryUsers(db, conditions, requiredCustomScopes)
  try {
    const response = {
      data: {
        counts: counts,
      },
    }
    res.json(response)
  } catch (e) {
    res.status(500).send({ message: e.message })
  }
}

module.exports = countUsers

# Offer evaluation API endpoint

We use this endpoint when finalizing offer redemption. This evaluation result will be sent to the Service Provider for them to give right benefits to users.

# API definition

API doc for offer evaluation

# Implementation

What you have to do by yourself is

  • Validate access token attached in the API request authorization header. To validate, use the introspectToken function in our SDK. The scope you need to check is individual:read_evaluated_offer
  • Get the credify ID, condition, required custom scopes from the request
  • Fetch the claim object associated with the credify user, using the same flow as previously in Push claim flow. By now, you should have credify ID mapped with your local user ID. Please refer to the implementation in our skeleton for this part.
  • Evaluate the user's level based on their profile and provided offer conditions using the evaluateOffer function in our SDK
  • Return the evaluation result
  • Node.js
  • Java
  • .NET
const evaluate = async (req, res, { db, credify }) => {
  if (process.env.CONTEXT_ENV !== "Jest") {
    const token = extractToken(req)
    try {
      const validToken = await credify.auth.introspectToken(
        token,
        PERMISSION_SCOPE.READ_EVALUATED_OFFER
      )
      if (!validToken) {
        return res.status(401).send({ message: "Unauthorized" })
      }
    } catch (e) {
      return res.status(500).send({ message: e.message })
    }
  }

  if (!req.body.credify_id || !req.body.scopes) {
    return res.status(400).send({ message: "Invalid body" })
  }
  let conditions = req.body.conditions || [{}]
  conditions = conditions.map((c) => {
    return c === null ? {} : c
  })
  const requiredCustomScopes = req.body.required_custom_scopes || []

  try {
    const credifyId = req.body.credify_id;
    const sharedScopes = req.body.scopes
    const userSharedClaims = await fetchUserClaimObject(db, undefined, credifyId, sharedScopes, false);

    const result = await credify.offer.evaluateOffer(
      conditions,
      requiredCustomScopes,
      userSharedClaims
    )
    const response = {
      data: {
        rank: result.rank,
        used_scopes: result.usedScopes,
        requested_scopes: result.requestedScopes,
      },
    }
    res.json(response)
  } catch (e) {
    res.status(500).send({ message: e.message })
  }
}

module.exports = evaluate

# Encrypted claims API endpoint

This API is used to encrypt user data with the Financial Institution's key and pass the encrypted data to the Financial Institution through OpenID Connect. This helps ensure that the user's data remains secure and private during transmission to the Financial Institution. The API is called after the user has agreed to share their data, and can be used to help protect sensitive information from unauthorized access.

# API definition

API doc for Encrypted claims API

# Implementation

What you have to do by yourself is

  • Validate access token attached in the API request authorization header. To validate, use the introspectToken function in our SDK. The scope you need to check is oidc_client:read_encrypted_claims
  • Extract the requestToken and the approvalToken from the request
  • Use the validateRequest function in our SDK to validate the accessToken, requestToken, approvalToken. This will return the publicKey and scopes of the request. Save these 2 values for the next steps.
  • Fetch the claim object associated with the credify user, using the same flow as previously in Push claim flow. Then use the encrypt claim function in our SDK to encrypt the claim object with the publicKey you got earlier
  • Get the verification info of your user (an example of this implementation can be found in our skeleton), and the encryptedClaims from the previous step, and return the result.

The below code is an example of how a claim object composer look like. We recommend you to follow the format of the object below so a Service Provider can decrypt the claims given from your side without any unexpected issues.

const claims = {};
claims["b09e8f99-6d89-4e7d-83ea-a43a1787b3e0:market-name-history-data"] = {
  "3285592c-9aaf-4182-bff5-941ce5dac483:purchase-count": `${user.transactionsCount}`,
  "3285592c-9aaf-4182-bff5-941ce5dac483:total-payment-amount": `${user.totalPaymentAmount}`,
  "b09e8f99-6d89-4e7d-83ea-a43a1787b3e0:market-name-history-data:commitment": "some randon value"
};
claims["b09e8f99-6d89-4e7d-83ea-a43a1787b3e0:market-name%20score"] = {
  "3285592c-9aaf-4182-bff5-941ce5dac483:market-name%20score": `${user.creditScore}`,
  "b09e8f99-6d89-4e7d-83ea-a43a1787b3e0:market-name%20score:commitment": "some random value"
};

 // This is the sample response object after processing the claim object
const data = {
  _claim_names: {
    '3285592c-9aaf-4182-bff5-941ce5dac483:purchase-count': 'b09e8f99-6d89-4e7d-83ea-a43a1787b3e0:market-name-history-data',
    '3285592c-9aaf-4182-bff5-941ce5dac483:total-payment-amount': 'b09e8f99-6d89-4e7d-83ea-a43a1787b3e0:market-name-history-data',
    'b09e8f99-6d89-4e7d-83ea-a43a1787b3e0:market-name-history-data:commitment': 'b09e8f99-6d89-4e7d-83ea-a43a1787b3e0:market-name-history-data',
    '3285592c-9aaf-4182-bff5-941ce5dac483:market-name%20score': 'b09e8f99-6d89-4e7d-83ea-a43a1787b3e0:market-name%20score',
    'b09e8f99-6d89-4e7d-83ea-a43a1787b3e0:market-name%20score:commitment': 'b09e8f99-6d89-4e7d-83ea-a43a1787b3e0:market-name%20score'
  },
  _claim_resources: {
    'b09e8f99-6d89-4e7d-83ea-a43a1787b3e0:market-name-history-data': {
      JWT: "JWE encoding the claim object"
    },
    'b09e8f99-6d89-4e7d-83ea-a43a1787b3e0:market-name%20score': {
      JWT: "JWE encoding the claim object"
    }
  }
}
  • Node.js
  • Java
  • .NET
const encryptClaims = async (req, res, { db, credify }) => {
  let publicKey = "";
  let scopes = [];

  if (process.env.CONTEXT_ENV !== "Jest") {
    const accessToken = extractToken(req)

    if (accessToken === "") {
      return res.status(401).send({ message: "Unauthorized" })
    }

    if (
      !req.body.user_id ||
      !req.body.request_token ||
      !req.body.approval_token
    ) {
      return res.status(400).send({ message: "Invalid body" })
    }

    const requestToken = req.body.request_token
    const approvalToken = req.body.approval_token

    try {
      const result = await credify.claims.validateRequest(
        accessToken,
        requestToken,
        approvalToken
      )
      publicKey = result.publicKey;
      scopes = result.scopes;
    } catch (e) {
      return res.status(500).send({ message: e.message })
    }

  } else {
    publicKey = req.headers["public-key"];
    scopes = req.headers["scopes"].split(",");
  }

  try {
    const credifyId = req.body.user_id

    const claims = await fetchUserClaimObject(db, undefined, credifyId, scopes, true);
    const encrypted = await credify.claims.encrypt(claims, publicKey)
    // please check our skeleton for implementation of fetchVerificationInfo
    const verificationInfo = await fetchVerificationInfo(db, credifyId);
    const data = {
      data: {
        verification_info: verificationInfo,
        claims: encrypted,
      },
    }
    res.send(data)
  } catch (e) {
    res.status(500).send({message: e.message})
  }
}

module.exports = encryptClaims

# Offer filtering API endpoint

The Offer Filtering API provides a list of available offers for each user with evaluation result. This is needed for the Credify SDK to efficiently display the offers on your platform.

# API definition

API doc for Offer filtering API

# Implementation

What you have to do by yourself is

  • Validate access token attached in the API request authorization header. To validate, use the introspectTokenReturnResult function in our SDK. The scope you need to check is claim_provider:read_filtered_offers. Be careful, we use a different function here, and you need to check the token extra carefully here. Example code:
const isCorrectAudience = tokenResult.data.aud[0].includes("/api/offers/filter")
const isCorrectScope = tokenResult.data.scope.split(" ").includes(PERMISSION_SCOPE.READ_FILTER_OFFER)
if (!tokenResult.data.active || !isCorrectAudience || !isCorrectScope) {
  return res.status(401).send({message: "Unauthorized"})
}
  • Get the credifyID, localID, and offers from the request
  • Fetch the claim object associated with the credify user, using the same flow as previously in Push claim flow.
  • Use the evaluateOffer function in our SDK then evaluate the claim object, the offer's condition, and offer's required custom scopes, then return the result
  • Node.js
  • Java
  • .NET
const filterOffer = async (req, res, { db, credify }) => {
  if (process.env.CONTEXT_ENV !== "Jest") {
    try {
      const token = extractToken(req)
      const tokenResult = await credify.auth.introspectTokenReturnResult(
        token,
        PERMISSION_SCOPE.READ_FILTER_OFFER
      )
      // check token is active, has correct audience, has correct scope
      const isCorrectAudience = tokenResult.data.aud[0].includes("/api/offers/filter")
      const isCorrectScope = tokenResult.data.scope.split(" ").includes(PERMISSION_SCOPE.READ_FILTER_OFFER)
      if (!tokenResult.data.active || !isCorrectAudience || !isCorrectScope) {
        return res.status(401).send({message: "Unauthorized"})
      }
    } catch (e) {
      return res.status(500).send({message: e.message})
    }
  }

  const credifyId = req.body.credify_id
  const localId = req.body.local_id
  const offers = req.body.offers

  if (!credifyId && !localId) {
    return res.status(400).send({ message: "No ID found" })
  }
  if (offers === undefined) {
    const response = {
      data: {
        offers: [],
      },
    }
    return res.status(200).json(response)
  }

  try {
    if (!offers.length) {
      const response = {
        data: {
          offers: [],
        },
      }
      return res.status(200).json(response)
    }

    const userClaims = await fetchUserClaimObject(db, localId, credifyId, [], false);
    const personalizedOffers = []

    await Promise.all((offers.map(async (offer) => {
      const result = await credify.offer.evaluateOffer(
        offer.conditions,
        offer.required_custom_scopes || [],
        userClaims
      )

      console.log("evaluate offer res: ", JSON.stringify(result))

      const formattedOffer = {
        ...offer,
        evaluation_result: {
          rank: result.rank,
          used_scopes: result.usedScopes,
          requested_scopes: result.requestedScopes,
        },
      }

      if (result.rank > 0) {
        // Return only qualified offers
        personalizedOffers.push(formattedOffer)
      }
    })))

    const response = {
      data: {
        offers: personalizedOffers,
      },
    }
    res.json(response)
  } catch (e) {
    res.status(500).send({ message: e.message })
  }
}

module.exports = filterOffer

Last Updated: 3/29/2023, 3:40:50 AM