POST /credentials/issue
Emite credenciales a uno o múltiples recipientes.
- 1 recipiente: Emisión síncrona. Retorna la credencial inmediatamente.
- 2+ recipientes: Emisión asíncrona. Retorna un
job_idpara consultar el progreso.
Request
POST /api/v1/credentials/issue
Headers
| Header | Requerido | Descripción |
|---|---|---|
X-API-Key | Si | Tu API key |
Content-Type | Si | application/json |
Body
{
"template_id": "550e8400-e29b-41d4-a716-446655440000",
"recipients": [
{
"email": "[email protected]",
"recipient_name": "Juan Perez",
"result_value": "A",
"credits_earned": 6.0
}
]
}
Campos del recipiente
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
email | string | Siempre | Email del recipiente |
recipient_name | string | Según template | Nombre completo |
result_value | string | Según template | Calificación/resultado |
credits_earned | number | Según template | Créditos obtenidos |
evidence | array | Según template | Evidencia del trabajo |
role | string | Según template | Rol del recipiente |
license_number | string | Según template | Número de licencia |
term | string | Según template | Período académico |
activity_start_date | string | Según template | Fecha inicio (YYYY-MM-DD) |
activity_end_date | string | Según template | Fecha fin (YYYY-MM-DD) |
Estructura de evidence
{
"evidence": [
{
"name": "Proyecto Final",
"url": "https://ejemplo.com/proyecto",
"description": "Implementacion de API REST en Python"
}
]
}
Consulta GET /templates/\{id\} para saber que campos son requeridos por cada template.
Response: Emisión síncrona (1 recipiente)
{
"mode": "sync",
"credentials": [
{
"email": "[email protected]",
"status": "issued",
"credential_id": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
"credential_url": "https://app.unicreda.com/credential/7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d"
}
]
}
Response: Emisión asíncrona (2+ recipientes)
{
"mode": "async",
"job_id": "abc123-task-id",
"message": "Issuing 50 credentials in background. Poll GET /api/v1/jobs/abc123-task-id for status."
}
Usa GET /jobs/\{job_id\} para consultar el progreso.
Detección automática interno/externo
La API detecta automáticamente si el email corresponde a un miembro de tu organización:
- Interno: Se enlazan automáticamente al wallet del usuario + se auto-llenan campos del perfil
- Externo: Se crea la credencial y se envia por email con un enlace publico
Errores por recipiente
Si un recipiente tiene datos inválidos en modo sincrono:
{
"mode": "sync",
"credentials": [
{
"email": "[email protected]",
"status": "error",
"errors": ["Calificación inválida. Valores permitidos: A, B, C, D, F"]
}
]
}
Idempotency
Cada job_id actúa como idempotency key implícito del lote.
Si el sistema reintenta procesar el mismo job (worker reinicia, glitch de red, redelivery del broker), las credenciales ya emitidas no se duplican: la base de datos garantiza unicidad por (achievement_id, email_normalizado, job_id). El email se normaliza con la misma lógica que usamos para detectar duplicados dentro de un lote (lowercase + sin dots/aliases para Gmail).
En el response de GET /jobs/\{job_id\} aparece el campo already_emitted con el número de recipients que se detectaron como ya emitidos en una entrega anterior. No se cuenta como error.
{
"job_id": "abc123-task-id",
"status": "SUCCESS",
"result": {
"total": 50,
"successful": 48,
"failed": 0,
"already_emitted": 2,
"errors": [],
"emails_sent": 48,
"emails_failed": 0,
"internal_count": 10,
"external_count": 40
}
}
Llamar POST /credentials/issue dos veces con los mismos recipients pero como dos requests distintos sí crea credenciales nuevas — cada request genera un job_id distinto. La idempotency es por job, no global. Si necesitás idempotency cross-request, deduplicá del lado del integrador antes de hacer el POST.
Arquitectura interna (informativa)
A partir de la versión actual, la emisión asíncrona puede correr en una de dos rutas internas, controladas server-side por un feature flag:
- Ruta legacy: el worker procesa todas las filas en un solo task con un commit final.
- Ruta chord: el worker parte el lote en chunks de 100 filas que corren con su propio commit; un callback final agrega resultados, corre rules de pathway y envía los emails. Si un chunk falla por un transitorio (DB lenta, glitch de red), solo ese chunk se reintenta hasta 3 veces con backoff de 1, 5 y 15 minutos.
Para el integrador no hay diferencia visible: el mode, el job_id, el polling, y el shape del response del result son idénticos. La única diferencia perceptible es la granularidad de progress.current (ver GET /jobs/{id}): en la ruta chord, el contador avanza en saltos de 100 (cuando un chunk termina), en lugar de fila por fila.
El switch entre rutas no rompe la API ni cambia la semántica de idempotency descrita arriba.
Ejemplo: Emisión masiva con Python
import requests
import time
API_KEY = "uc_live_tu_key_aqui"
BASE_URL = "https://app.unicreda.com/api/v1"
headers = {"X-API-Key": API_KEY, "Content-Type": "application/json"}
# Emitir a múltiples recipientes
response = requests.post(f"{BASE_URL}/credentials/issue", headers=headers, json={
"template_id": "550e8400-e29b-41d4-a716-446655440000",
"recipients": [
{"email": f"estudiante{i}@ejemplo.com", "recipient_name": f"Estudiante {i}", "result_value": "A"}
for i in range(1, 51)
]
})
data = response.json()
if data["mode"] == "async":
job_id = data["job_id"]
# Polling del progreso
while True:
status = requests.get(f"{BASE_URL}/jobs/{job_id}", headers=headers).json()
print(f"Status: {status['status']}")
if status["status"] == "SUCCESS":
print(f"Resultado: {status['result']}")
break
elif status["status"] == "FAILURE":
print(f"Error: {status['error']}")
break
time.sleep(2) # Esperar 2 segundos entre consultas